diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs
index 898fc935b0..7414054b53 100644
--- a/Terminal.Gui/Application/Application.Run.cs
+++ b/Terminal.Gui/Application/Application.Run.cs
@@ -1,7 +1,6 @@
#nullable enable
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using Microsoft.CodeAnalysis.Diagnostics;
namespace Terminal.Gui;
@@ -522,7 +521,7 @@ public static void LayoutAndDraw (bool forceDraw = false)
/// The driver for the application
/// The main loop.
- internal static MainLoop? MainLoop { get; private set; }
+ public static MainLoop? MainLoop { get; private set; }
///
/// Set to true to cause to be called after the first iteration. Set to false (the default) to
diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs
index ee4bba220c..9ee974c7c9 100644
--- a/Terminal.Gui/Application/MainLoop.cs
+++ b/Terminal.Gui/Application/MainLoop.cs
@@ -10,7 +10,7 @@
namespace Terminal.Gui;
/// Interface to create a platform specific driver.
-internal interface IMainLoopDriver
+public interface IMainLoopDriver
{
/// Must report whether there are any events pending, or even block waiting for events.
/// true, if there were pending events, false otherwise.
@@ -29,6 +29,16 @@ internal interface IMainLoopDriver
/// Wakes up the that might be waiting on input, must be thread safe.
void Wakeup ();
+
+ ///
+ /// Flag to force reading input instead of call .
+ ///
+ bool ForceRead { get; set; }
+
+ ///
+ /// Switch for waiting for input or signaled to resume.
+ ///
+ ManualResetEventSlim WaitForInput { get; set; }
}
/// The MainLoop monitors timers and idle handlers.
@@ -36,7 +46,7 @@ internal interface IMainLoopDriver
/// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this
/// on Windows.
///
-internal class MainLoop : IDisposable
+public class MainLoop : IDisposable
{
internal List> _idleHandlers = new ();
internal SortedList _timeouts = new ();
@@ -72,7 +82,7 @@ internal ReadOnlyCollection> IdleHandlers
/// The current in use.
/// The main loop driver.
- internal IMainLoopDriver MainLoopDriver { get; private set; }
+ public IMainLoopDriver MainLoopDriver { get; private set; }
/// Used for unit tests.
internal bool Running { get; set; }
@@ -130,10 +140,7 @@ internal Func AddIdle (Func idleHandler)
///
internal object AddTimeout (TimeSpan time, Func callback)
{
- if (callback is null)
- {
- throw new ArgumentNullException (nameof (callback));
- }
+ ArgumentNullException.ThrowIfNull (callback);
var timeout = new Timeout { Span = time, Callback = callback };
AddTimeout (time, timeout);
@@ -156,7 +163,7 @@ internal bool CheckTimersAndIdleHandlers (out int waitTimeout)
waitTimeout = 0;
- lock (_timeouts)
+ lock (_timeoutsLockToken)
{
if (_timeouts.Count > 0)
{
@@ -262,7 +269,7 @@ internal void Run ()
///
internal void RunIteration ()
{
- lock (_timeouts)
+ lock (_timeoutsLockToken)
{
if (_timeouts.Count > 0)
{
@@ -272,7 +279,7 @@ internal void RunIteration ()
MainLoopDriver.Iteration ();
- var runIdle = false;
+ bool runIdle;
lock (_idleHandlersLock)
{
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
new file mode 100644
index 0000000000..2027beab02
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
@@ -0,0 +1,126 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Describes an ongoing ANSI request sent to the console.
+/// Send a request using which will return the response.
+///
+public class AnsiEscapeSequenceRequest
+{
+ internal readonly object _responseLock = new (); // Per-instance lock
+
+ ///
+ /// Gets the response received from the request.
+ ///
+ public AnsiEscapeSequenceResponse? AnsiEscapeSequenceResponse { get; internal set; }
+
+ ///
+ /// The value expected in the response after the CSI e.g.
+ ///
+ /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
+ ///
+ /// should result in a response of the form ESC [ 8 ; height ; width t. In this case,
+ ///
+ /// will be "8".
+ ///
+ public string? ExpectedResponseValue { get; init; }
+
+ ///
+ /// Gets the request string to send e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ ///
+ public required string Request { get; init; }
+
+ ///
+ ///
+ /// Gets the terminator that uniquely identifies the response received from
+ /// the console. e.g. for
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// the terminator is
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator
+ ///
+ /// .
+ ///
+ ///
+ /// After sending a request, the first response with matching terminator will be matched
+ /// to the oldest outstanding request.
+ ///
+ ///
+ public required string Terminator { get; init; }
+
+ internal void RaiseResponseFromInput (string? response, AnsiEscapeSequenceRequest? request)
+ {
+ ProcessResponse (response, request);
+
+ ResponseFromInput?.Invoke (this, AnsiEscapeSequenceResponse);
+ }
+
+ ///
+ /// Raised with the response object and validation.
+ ///
+ internal event EventHandler? ResponseFromInput;
+
+ ///
+ /// Process the of an ANSI escape sequence request.
+ ///
+ /// The response.
+ private void ProcessResponse (string? response, AnsiEscapeSequenceRequest? request)
+ {
+ var error = new StringBuilder ();
+ var values = new string? [] { null };
+
+ try
+ {
+ if (!string.IsNullOrEmpty (response) && !response.StartsWith (AnsiEscapeSequenceRequestUtils.KeyEsc))
+ {
+ throw new InvalidOperationException ($"Invalid Response: {response}");
+ }
+
+ if (string.IsNullOrEmpty (Terminator))
+ {
+ throw new InvalidOperationException ("Terminator request is empty.");
+ }
+
+ if (string.IsNullOrEmpty (response))
+ {
+ throw new InvalidOperationException ("Response request is null.");
+ }
+
+ if (!string.IsNullOrEmpty (response) && !response.EndsWith (Terminator [^1]))
+ {
+ string resp = string.IsNullOrEmpty (response) ? "" : response.Last ().ToString ();
+
+ throw new InvalidOperationException ($"Terminator ends with '{resp}'\nand doesn't end with: '{Terminator [^1]}'");
+ }
+ }
+ catch (Exception ex)
+ {
+ error.AppendLine ($"Error executing ANSI request:\n{ex.Message}");
+ }
+ finally
+ {
+ if (string.IsNullOrEmpty (error.ToString ()))
+ {
+ (string? _, string? _, values, string? _) = AnsiEscapeSequenceRequestUtils.GetEscapeResult (response?.ToCharArray ());
+ }
+
+ if (request is { } && !string.IsNullOrEmpty (request.ExpectedResponseValue) && request.ExpectedResponseValue != values [0])
+ {
+ error.AppendLine ($"Error executing ANSI request:\nValue ends with '{values [0]}'\nand doesn't end with: '{ExpectedResponseValue! [^1]}'");
+ }
+ }
+
+ AnsiEscapeSequenceResponse = new ()
+ {
+ Response = response, Error = error.ToString (),
+ Terminator = string.IsNullOrEmpty (response) ? "" : response [^1].ToString (),
+ ExpectedResponseValue = values [0],
+ Valid = string.IsNullOrWhiteSpace (error.ToString ()) && !string.IsNullOrWhiteSpace (response)
+ };
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestStatus.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestStatus.cs
new file mode 100644
index 0000000000..b07d482366
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestStatus.cs
@@ -0,0 +1,17 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Represents the status of an ANSI escape sequence request made to the terminal using
+/// .
+///
+///
+public class AnsiEscapeSequenceRequestStatus
+{
+ /// Creates a new state of escape sequence request.
+ /// The object.
+ public AnsiEscapeSequenceRequestStatus (AnsiEscapeSequenceRequest ansiRequest) { AnsiRequest = ansiRequest; }
+
+ /// Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).
+ public AnsiEscapeSequenceRequest AnsiRequest { get; }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestUtils.cs
similarity index 61%
rename from Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
rename to Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestUtils.cs
index 6330c33702..6eef22931a 100644
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequestUtils.cs
@@ -1,5 +1,16 @@
+#nullable enable
+using Terminal.Gui.ConsoleDrivers;
+using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
+
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
+
///
/// Provides a platform-independent API for managing ANSI escape sequences.
///
@@ -9,8 +20,9 @@ namespace Terminal.Gui;
/// * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
/// * https://vt100.net/
///
-public static class EscSeqUtils
+public static class AnsiEscapeSequenceRequestUtils
{
+ // TODO: One type per file - Move this enum to a separate file.
///
/// Options for ANSI ESC "[xJ" - Clears part of the screen.
///
@@ -37,6 +49,9 @@ public enum ClearScreenOptions
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; }
///
/// Escape key code (ASCII 27/0x1B).
///
@@ -122,19 +137,19 @@ public enum ClearScreenOptions
public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h";
//private static bool isButtonReleased;
- private static bool isButtonClicked;
+ private static bool _isButtonClicked;
- private static bool isButtonDoubleClicked;
+ private static bool _isButtonDoubleClicked;
//private static MouseFlags? lastMouseButtonReleased;
// QUESTION: What's the difference between isButtonClicked and isButtonPressed?
// Some clarity or comments would be handy, here.
// It also seems like some enforcement of valid states might be a good idea.
- private static bool isButtonPressed;
- private static bool isButtonTripleClicked;
+ private static bool _isButtonPressed;
+ private static bool _isButtonTripleClicked;
- private static MouseFlags? lastMouseButtonPressed;
- private static Point? point;
+ private static MouseFlags? _lastMouseButtonPressed;
+ private static Point? _point;
///
/// Control sequence for disabling mouse events.
@@ -155,10 +170,14 @@ public enum ClearScreenOptions
///
public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; }
+ ///
+ /// Specify the incomplete array not yet recognized as valid ANSI escape sequence.
+ ///
+ public static ConsoleKeyInfo []? IncompleteCkInfos { get; set; }
+
///
/// Decodes an ANSI escape sequence.
///
- /// The which may contain a request.
/// The which may change.
/// The which may change.
/// The array.
@@ -170,10 +189,9 @@ public enum ClearScreenOptions
/// Indicates if the escape sequence is a mouse event.
/// The button state.
/// The position.
- /// Indicates if the escape sequence is a response to a request.
+ /// The object.
/// The handler that will process the event.
public static void DecodeEscSeq (
- EscSeqRequests escSeqRequests,
ref ConsoleKeyInfo newConsoleKeyInfo,
ref ConsoleKey key,
ConsoleKeyInfo [] cki,
@@ -185,17 +203,17 @@ public static void DecodeEscSeq (
out bool isMouse,
out List buttonState,
out Point pos,
- out bool isResponse,
+ out AnsiEscapeSequenceRequestStatus? seqReqStatus,
Action continuousButtonPressedHandler
)
{
char [] kChars = GetKeyCharArray (cki);
(c1Control, code, values, terminator) = GetEscapeResult (kChars);
isMouse = false;
- buttonState = new List { 0 };
+ buttonState = [0];
pos = default (Point);
- isResponse = false;
- char keyChar = '\0';
+ seqReqStatus = null;
+ var keyChar = '\0';
switch (c1Control)
{
@@ -204,53 +222,112 @@ Action continuousButtonPressedHandler
{
key = ConsoleKey.Escape;
- newConsoleKeyInfo = new ConsoleKeyInfo (
- cki [0].KeyChar,
- key,
- (mod & ConsoleModifiers.Shift) != 0,
- (mod & ConsoleModifiers.Alt) != 0,
- (mod & ConsoleModifiers.Control) != 0);
+ newConsoleKeyInfo = new (
+ cki [0].KeyChar,
+ key,
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
}
- else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26)
+ else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26 && (uint)cki [1].KeyChar != '\n' && (uint)cki [1].KeyChar != '\r')
{
key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1);
+ mod = ConsoleModifiers.Alt | ConsoleModifiers.Control;
+
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ key,
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
+ }
+ else if (cki [1].KeyChar >= 65 && cki [1].KeyChar <= 90)
+ {
+ key = (ConsoleKey)cki [1].KeyChar;
+ mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt;
+
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ (ConsoleKey)Math.Min ((uint)key, 255),
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
+ }
+ else if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122)
+ {
+ key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0];
+ mod = ConsoleModifiers.Alt;
+
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ (ConsoleKey)Math.Min ((uint)key, 255),
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
+ }
+ else if (cki [1].KeyChar is '\0' or ' ')
+ {
+ key = ConsoleKey.Spacebar;
+
+ if (kChars.Length > 1 && kChars [1] == '\0')
+ {
+ mod = ConsoleModifiers.Alt | ConsoleModifiers.Control;
+ }
+ else
+ {
+ mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt;
+ }
- newConsoleKeyInfo = new ConsoleKeyInfo (
- cki [1].KeyChar,
- key,
- false,
- true,
- true);
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ (ConsoleKey)Math.Min ((uint)key, 255),
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
}
- else
+ else if (cki [1].KeyChar is '\n' or '\r')
{
- if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122)
+ key = ConsoleKey.Enter;
+
+ if (kChars.Length > 1 && kChars [1] == '\n')
{
- key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0];
+ mod = ConsoleModifiers.Alt | ConsoleModifiers.Control;
}
else
{
- key = (ConsoleKey)cki [1].KeyChar;
+ mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt;
}
- newConsoleKeyInfo = new ConsoleKeyInfo (
- (char)key,
- (ConsoleKey)Math.Min ((uint)key, 255),
- false,
- true,
- false);
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ (ConsoleKey)Math.Min ((uint)key, 255),
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
+ }
+ else
+ {
+ key = (ConsoleKey)cki [1].KeyChar;
+ mod = ConsoleModifiers.Alt;
+
+ newConsoleKeyInfo = new (
+ cki [1].KeyChar,
+ (ConsoleKey)Math.Min ((uint)key, 255),
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
}
break;
case "SS3":
key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar);
- newConsoleKeyInfo = new ConsoleKeyInfo (
- keyChar,
- key,
- (mod & ConsoleModifiers.Shift) != 0,
- (mod & ConsoleModifiers.Alt) != 0,
- (mod & ConsoleModifiers.Control) != 0);
+ newConsoleKeyInfo = new (
+ keyChar,
+ key,
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
break;
case "CSI":
@@ -262,16 +339,24 @@ Action continuousButtonPressedHandler
return;
}
- if (escSeqRequests is { } && escSeqRequests.HasResponse (terminator))
+ if (AnsiEscapeSequenceRequests.HasResponse (terminator, out seqReqStatus))
{
- isResponse = true;
- escSeqRequests.Remove (terminator);
+ AnsiEscapeSequenceRequests.Remove (seqReqStatus);
+
+ var ckiString = ToString (cki);
+
+ lock (seqReqStatus?.AnsiRequest._responseLock!)
+ {
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (ckiString, seqReqStatus.AnsiRequest);
+ }
return;
}
if (!string.IsNullOrEmpty (terminator))
{
+ System.Diagnostics.Debug.Assert (terminator.Length == 1);
+
key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar);
if (key != 0 && values.Length > 1)
@@ -279,31 +364,48 @@ Action continuousButtonPressedHandler
mod |= GetConsoleModifiers (values [1]);
}
- newConsoleKeyInfo = new ConsoleKeyInfo (
- keyChar,
- key,
- (mod & ConsoleModifiers.Shift) != 0,
- (mod & ConsoleModifiers.Alt) != 0,
- (mod & ConsoleModifiers.Control) != 0);
+ if (keyChar != 0 || key != 0 || mod != 0)
+ {
+ newConsoleKeyInfo = new (
+ keyChar,
+ key,
+ (mod & ConsoleModifiers.Shift) != 0,
+ (mod & ConsoleModifiers.Alt) != 0,
+ (mod & ConsoleModifiers.Control) != 0);
+ }
+ else
+ {
+ // It's request response that wasn't handled by a valid request terminator
+ System.Diagnostics.Debug.Assert (AnsiEscapeSequenceRequests.Statuses.Count > 0);
+
+ if (AnsiEscapeSequenceRequests.Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus? result))
+ {
+ lock (result.AnsiRequest._responseLock)
+ {
+ result.AnsiRequest.RaiseResponseFromInput (ToString (cki), result.AnsiRequest);
+ }
+ }
+ }
}
else
{
// BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/2803
// This is caused by NetDriver depending on Console.KeyAvailable?
- throw new InvalidOperationException ("CSI response, but there's no terminator");
+ //throw new InvalidOperationException ("CSI response, but there's no terminator");
- //newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
- // key,
- // (mod & ConsoleModifiers.Shift) != 0,
- // (mod & ConsoleModifiers.Alt) != 0,
- // (mod & ConsoleModifiers.Control) != 0);
+ IncompleteCkInfos = cki;
}
+ break;
+ default:
+ newConsoleKeyInfo = MapConsoleKeyInfo (cki [0]);
+ key = newConsoleKeyInfo.Key;
+ mod = newConsoleKeyInfo.Modifiers;
+
break;
}
}
- #nullable enable
///
/// Gets the c1Control used in the called escape sequence.
///
@@ -335,6 +437,7 @@ public static string GetC1ControlChar (in char c)
};
}
+
///
/// Gets the depending on terminating and value.
///
@@ -369,6 +472,7 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons
('B', _) => ConsoleKey.DownArrow,
('C', _) => ConsoleKey.RightArrow,
('D', _) => ConsoleKey.LeftArrow,
+ ('E', _) => ConsoleKey.Clear,
('F', _) => ConsoleKey.End,
('H', _) => ConsoleKey.Home,
('P', _) => ConsoleKey.F1,
@@ -388,18 +492,19 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons
('~', "21") => ConsoleKey.F10,
('~', "23") => ConsoleKey.F11,
('~', "24") => ConsoleKey.F12,
- ('l', _) => ConsoleKey.Add,
- ('m', _) => ConsoleKey.Subtract,
- ('p', _) => ConsoleKey.Insert,
- ('q', _) => ConsoleKey.End,
- ('r', _) => ConsoleKey.DownArrow,
- ('s', _) => ConsoleKey.PageDown,
- ('t', _) => ConsoleKey.LeftArrow,
- ('u', _) => ConsoleKey.Clear,
- ('v', _) => ConsoleKey.RightArrow,
- ('w', _) => ConsoleKey.Home,
- ('x', _) => ConsoleKey.UpArrow,
- ('y', _) => ConsoleKey.PageUp,
+ // These terminators are used by macOS on a numeric keypad without keys modifiers
+ ('l', null) => ConsoleKey.Add,
+ ('m', null) => ConsoleKey.Subtract,
+ ('p', null) => ConsoleKey.Insert,
+ ('q', null) => ConsoleKey.End,
+ ('r', null) => ConsoleKey.DownArrow,
+ ('s', null) => ConsoleKey.PageDown,
+ ('t', null) => ConsoleKey.LeftArrow,
+ ('u', null) => ConsoleKey.Clear,
+ ('v', null) => ConsoleKey.RightArrow,
+ ('w', null) => ConsoleKey.Home,
+ ('x', null) => ConsoleKey.UpArrow,
+ ('y', null) => ConsoleKey.PageUp,
(_, _) => 0
};
}
@@ -434,7 +539,7 @@ public static ConsoleModifiers GetConsoleModifiers (string? value)
///
public static (string c1Control, string code, string [] values, string terminating) GetEscapeResult (char [] kChar)
{
- if (kChar is null || kChar.Length == 0)
+ if (kChar is null || kChar.Length == 0 || (kChar.Length == 1 && kChar [0] != KeyEsc))
{
return (null, null, null, null);
}
@@ -497,7 +602,7 @@ public static (string c1Control, string code, string [] values, string terminati
// PERF: This is expensive
public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki)
{
- char [] kChar = { };
+ char [] kChar = [];
var length = 0;
foreach (ConsoleKeyInfo kc in cki)
@@ -774,36 +879,36 @@ Action continuousButtonPressedHandler
mouseFlags = [MouseFlags.AllEvents];
- if (lastMouseButtonPressed != null
- && !isButtonPressed
+ if (_lastMouseButtonPressed != null
+ && !_isButtonPressed
&& !buttonState.HasFlag (MouseFlags.ReportMousePosition)
&& !buttonState.HasFlag (MouseFlags.Button1Released)
&& !buttonState.HasFlag (MouseFlags.Button2Released)
&& !buttonState.HasFlag (MouseFlags.Button3Released)
&& !buttonState.HasFlag (MouseFlags.Button4Released))
{
- lastMouseButtonPressed = null;
- isButtonPressed = false;
+ _lastMouseButtonPressed = null;
+ _isButtonPressed = false;
}
- if ((!isButtonClicked
- && !isButtonDoubleClicked
+ if ((!_isButtonClicked
+ && !_isButtonDoubleClicked
&& (buttonState == MouseFlags.Button1Pressed
|| buttonState == MouseFlags.Button2Pressed
|| buttonState == MouseFlags.Button3Pressed
|| buttonState == MouseFlags.Button4Pressed)
- && lastMouseButtonPressed is null)
- || (isButtonPressed && lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition)))
+ && _lastMouseButtonPressed is null)
+ || (_isButtonPressed && _lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition)))
{
mouseFlags [0] = buttonState;
- lastMouseButtonPressed = buttonState;
- isButtonPressed = true;
+ _lastMouseButtonPressed = buttonState;
+ _isButtonPressed = true;
- point = pos;
+ _point = pos;
if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0)
{
- Application.MainLoop.AddIdle (
+ Application.MainLoop?.AddIdle (
() =>
{
// INTENT: What's this trying to do?
@@ -818,7 +923,7 @@ Action continuousButtonPressedHandler
}
else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition))
{
- point = pos;
+ _point = pos;
// The isButtonPressed must always be true, otherwise we can lose the feature
// If mouse flags has ReportMousePosition this feature won't run
@@ -826,27 +931,27 @@ Action continuousButtonPressedHandler
//isButtonPressed = false;
}
}
- else if (isButtonDoubleClicked
+ else if (_isButtonDoubleClicked
&& (buttonState == MouseFlags.Button1Pressed
|| buttonState == MouseFlags.Button2Pressed
|| buttonState == MouseFlags.Button3Pressed
|| buttonState == MouseFlags.Button4Pressed))
{
mouseFlags [0] = GetButtonTripleClicked (buttonState);
- isButtonDoubleClicked = false;
- isButtonTripleClicked = true;
+ _isButtonDoubleClicked = false;
+ _isButtonTripleClicked = true;
}
- else if (isButtonClicked
+ else if (_isButtonClicked
&& (buttonState == MouseFlags.Button1Pressed
|| buttonState == MouseFlags.Button2Pressed
|| buttonState == MouseFlags.Button3Pressed
|| buttonState == MouseFlags.Button4Pressed))
{
mouseFlags [0] = GetButtonDoubleClicked (buttonState);
- isButtonClicked = false;
- isButtonDoubleClicked = true;
+ _isButtonClicked = false;
+ _isButtonDoubleClicked = true;
- Application.MainLoop.AddIdle (
+ Application.MainLoop?.AddIdle (
() =>
{
Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
@@ -866,26 +971,26 @@ Action continuousButtonPressedHandler
// });
//}
- else if (!isButtonClicked
- && !isButtonDoubleClicked
+ else if (!_isButtonClicked
+ && !_isButtonDoubleClicked
&& (buttonState == MouseFlags.Button1Released
|| buttonState == MouseFlags.Button2Released
|| buttonState == MouseFlags.Button3Released
|| buttonState == MouseFlags.Button4Released))
{
mouseFlags [0] = buttonState;
- isButtonPressed = false;
+ _isButtonPressed = false;
- if (isButtonTripleClicked)
+ if (_isButtonTripleClicked)
{
- isButtonTripleClicked = false;
+ _isButtonTripleClicked = false;
}
- else if (pos.X == point?.X && pos.Y == point?.Y)
+ else if (pos.X == _point?.X && pos.Y == _point?.Y)
{
mouseFlags.Add (GetButtonClicked (buttonState));
- isButtonClicked = true;
+ _isButtonClicked = true;
- Application.MainLoop.AddIdle (
+ Application.MainLoop?.AddIdle (
() =>
{
Task.Run (async () => await ProcessButtonClickedAsync ());
@@ -894,7 +999,7 @@ Action continuousButtonPressedHandler
});
}
- point = pos;
+ _point = pos;
//if ((lastMouseButtonPressed & MouseFlags.ReportMousePosition) == 0) {
// lastMouseButtonReleased = buttonState;
@@ -952,7 +1057,7 @@ Action continuousButtonPressedHandler
public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
{
ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
- ConsoleKey key;
+ ConsoleKey key = ConsoleKey.None;
char keyChar = consoleKeyInfo.KeyChar;
switch ((uint)keyChar)
@@ -960,56 +1065,176 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
case 0:
if (consoleKeyInfo.Key == (ConsoleKey)64)
{ // Ctrl+Space in Windows.
- newConsoleKeyInfo = new ConsoleKeyInfo (
- ' ',
- ConsoleKey.Spacebar,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ ConsoleKey.Spacebar,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ }
+ else if (consoleKeyInfo.Key == ConsoleKey.None)
+ {
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ ConsoleKey.Spacebar,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ true);
}
break;
- case uint n when n > 0 && n <= KeyEsc:
- if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r')
+ case uint n when n is > 0 and <= KeyEsc:
+ if (consoleKeyInfo is { Key: 0, KeyChar: '\u001B' })
+ {
+ key = ConsoleKey.Escape;
+
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ }
+ else if (consoleKeyInfo is { Key: 0, KeyChar: '\t' })
+ {
+ key = ConsoleKey.Tab;
+
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ }
+ else if (consoleKeyInfo is { Key: 0, KeyChar: '\r' })
+ {
+ key = ConsoleKey.Enter;
+
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ }
+ else if (consoleKeyInfo is { Key: 0, KeyChar: '\n' })
{
key = ConsoleKey.Enter;
- newConsoleKeyInfo = new ConsoleKeyInfo (
- consoleKeyInfo.KeyChar,
- key,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ true);
}
else if (consoleKeyInfo.Key == 0)
{
key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1);
- newConsoleKeyInfo = new ConsoleKeyInfo (
- (char)key,
- key,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
- true);
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ true);
}
break;
case 127: // DEL
- newConsoleKeyInfo = new ConsoleKeyInfo (
- consoleKeyInfo.KeyChar,
- ConsoleKey.Backspace,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
- (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
+ key = ConsoleKey.Backspace;
+
+ newConsoleKeyInfo = new (
+ consoleKeyInfo.KeyChar,
+ key,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
break;
default:
- newConsoleKeyInfo = consoleKeyInfo;
+ uint ck = MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey);
+
+ if (isConsoleKey)
+ {
+ key = (ConsoleKey)ck;
+ }
+
+ newConsoleKeyInfo = new (
+ keyChar,
+ key,
+ GetShiftMod (consoleKeyInfo.Modifiers),
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+ (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
break;
}
return newConsoleKeyInfo;
+
+ bool GetShiftMod (ConsoleModifiers modifiers)
+ {
+ if (consoleKeyInfo.KeyChar is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z && modifiers == ConsoleModifiers.None)
+ {
+ return true;
+ }
+
+ return (modifiers & ConsoleModifiers.Shift) != 0;
+ }
+ }
+
+ private static MouseFlags _lastMouseFlags;
+
+ ///
+ /// Provides a handler to be invoked when mouse continuous button pressed is processed.
+ ///
+ public static event EventHandler ContinuousButtonPressed;
+
+ ///
+ /// Provides a default mouse event handler that can be used by any driver.
+ ///
+ /// The mouse flags event.
+ /// The mouse position.
+ public static void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
+ {
+ bool WasButtonReleased (MouseFlags flag)
+ {
+ return flag.HasFlag (MouseFlags.Button1Released)
+ || flag.HasFlag (MouseFlags.Button2Released)
+ || flag.HasFlag (MouseFlags.Button3Released)
+ || flag.HasFlag (MouseFlags.Button4Released);
+ }
+
+ bool IsButtonNotPressed (MouseFlags flag)
+ {
+ return !flag.HasFlag (MouseFlags.Button1Pressed)
+ && !flag.HasFlag (MouseFlags.Button2Pressed)
+ && !flag.HasFlag (MouseFlags.Button3Pressed)
+ && !flag.HasFlag (MouseFlags.Button4Pressed);
+ }
+
+ bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
+ {
+ return flag.HasFlag (MouseFlags.Button1Clicked)
+ || flag.HasFlag (MouseFlags.Button2Clicked)
+ || flag.HasFlag (MouseFlags.Button3Clicked)
+ || flag.HasFlag (MouseFlags.Button4Clicked)
+ || flag.HasFlag (MouseFlags.Button1DoubleClicked)
+ || flag.HasFlag (MouseFlags.Button2DoubleClicked)
+ || flag.HasFlag (MouseFlags.Button3DoubleClicked)
+ || flag.HasFlag (MouseFlags.Button4DoubleClicked);
+ }
+
+ if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
+ {
+ return;
+ }
+
+ _lastMouseFlags = mouseFlag;
+
+ var me = new MouseEventArgs { Flags = mouseFlag, Position = pos };
+
+ ContinuousButtonPressed?.Invoke ((mouseFlag, pos), me);
}
///
@@ -1021,7 +1246,61 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
public static ConsoleKeyInfo [] ResizeArray (ConsoleKeyInfo consoleKeyInfo, ConsoleKeyInfo [] cki)
{
Array.Resize (ref cki, cki is null ? 1 : cki.Length + 1);
- cki [cki.Length - 1] = consoleKeyInfo;
+ cki [^1] = consoleKeyInfo;
+
+ return cki;
+ }
+
+ ///
+ /// Insert a array into the another array at the specified
+ /// index.
+ ///
+ /// The array to insert.
+ /// The array where will be added the array.
+ /// The start index to insert the array, default is 0.
+ /// The array with another array inserted.
+ public static ConsoleKeyInfo [] InsertArray ([CanBeNull] ConsoleKeyInfo [] toInsert, ConsoleKeyInfo [] cki, int index = 0)
+ {
+ if (toInsert is null)
+ {
+ return cki;
+ }
+
+ if (cki is null)
+ {
+ return toInsert;
+ }
+
+ if (index < 0)
+ {
+ index = 0;
+ }
+
+ ConsoleKeyInfo [] backupCki = cki.Clone () as ConsoleKeyInfo [];
+
+ Array.Resize (ref cki, cki.Length + toInsert.Length);
+
+ for (var i = 0; i < cki.Length; i++)
+ {
+ if (i == index)
+ {
+ for (var j = 0; j < toInsert.Length; j++)
+ {
+ cki [i] = toInsert [j];
+ i++;
+ }
+
+ for (int k = index; k < backupCki!.Length; k++)
+ {
+ cki [i] = backupCki [k];
+ i++;
+ }
+ }
+ else
+ {
+ cki [i] = backupCki! [i];
+ }
+ }
return cki;
}
@@ -1101,16 +1380,117 @@ private static MouseFlags GetButtonTripleClicked (MouseFlags mouseFlag)
return mf;
}
+ internal static 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.
+ System.Diagnostics.Debug.Assert (keyInfo.Key == 0);
+
+ 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);
+ }
+
private static async Task ProcessButtonClickedAsync ()
{
await Task.Delay (300);
- isButtonClicked = false;
+ _isButtonClicked = false;
}
private static async Task ProcessButtonDoubleClickedAsync ()
{
await Task.Delay (300);
- isButtonDoubleClicked = false;
+ _isButtonDoubleClicked = false;
}
private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action continuousButtonPressedHandler)
@@ -1118,7 +1498,7 @@ private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseF
// PERF: Pause and poll in a hot loop.
// This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent.
// Will make a massive difference in responsiveness.
- while (isButtonPressed)
+ while (_isButtonPressed)
{
await Task.Delay (100);
@@ -1129,9 +1509,9 @@ private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseF
break;
}
- if (isButtonPressed && lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
+ if (_isButtonPressed && _lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
{
- Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, point ?? Point.Empty));
+ Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, _point ?? Point.Empty));
}
}
}
@@ -1156,6 +1536,112 @@ private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlag
return mouseFlag;
}
+ ///
+ /// Split a raw string into a list of string with the correct ansi escape sequence.
+ ///
+ /// The raw string containing one or many ansi escape sequence.
+ /// A list with a valid ansi escape sequence.
+ public static List SplitEscapeRawString (string rawData)
+ {
+ List splitList = [];
+ var isEscSeq = false;
+ var split = string.Empty;
+ char previousChar = '\0';
+
+ for (var i = 0; i < rawData.Length; i++)
+ {
+ char c = rawData [i];
+
+ if (c == '\u001B')
+ {
+ isEscSeq = true;
+
+ split = AddAndClearSplit ();
+
+ split += c.ToString ();
+ }
+ else if (!isEscSeq && c >= Key.Space)
+ {
+ split = AddAndClearSplit ();
+ splitList.Add (c.ToString ());
+ }
+ else if ((previousChar != '\u001B' && c <= Key.Space) || (previousChar != '\u001B' && c == 127)
+ || (char.IsLetter (previousChar) && char.IsLower (c) && char.IsLetter (c))
+ || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsLetterOrDigit (c))
+ || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsPunctuation (c))
+ || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsSymbol (c)))
+ {
+ isEscSeq = false;
+ split = AddAndClearSplit ();
+ splitList.Add (c.ToString ());
+ }
+ else
+ {
+ split += c.ToString ();
+ }
+
+ if (!string.IsNullOrEmpty (split) && i == rawData.Length - 1)
+ {
+ splitList.Add (split);
+ }
+
+ previousChar = c;
+ }
+
+ return splitList;
+
+ string AddAndClearSplit ()
+ {
+ if (!string.IsNullOrEmpty (split))
+ {
+ splitList.Add (split);
+ split = string.Empty;
+ }
+
+ return split;
+ }
+ }
+
+ ///
+ /// Convert a array to string.
+ ///
+ ///
+ /// The string representing the array.
+ public static string ToString (ConsoleKeyInfo [] consoleKeyInfos)
+ {
+ StringBuilder sb = new ();
+
+ foreach (ConsoleKeyInfo keyChar in consoleKeyInfos)
+ {
+ sb.Append (keyChar.KeyChar);
+ }
+
+ return sb.ToString ();
+ }
+
+ ///
+ /// Convert a string to array.
+ ///
+ ///
+ /// The representing the string.
+ public static ConsoleKeyInfo [] ToConsoleKeyInfoArray (string ansi)
+ {
+ if (ansi is null)
+ {
+ return null;
+ }
+
+ ConsoleKeyInfo [] cki = new ConsoleKeyInfo [ansi.Length];
+
+ for (var i = 0; i < ansi.Length; i++)
+ {
+ char c = ansi [i];
+ cki [i] = new (c, 0, false, false, false);
+ }
+
+ return cki;
+ }
+
#region Cursor
//ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary*
@@ -1316,13 +1802,9 @@ public enum DECSCUSR_Style
///
/// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR)
/// https://terminalguide.namepad.de/seq/csi_sn__p-6/
+ /// The terminal reply to . ESC [ ? (y) ; (x) ; 1 R
///
- public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n";
-
- ///
- /// The terminal reply to . ESC [ ? (y) ; (x) R
- ///
- public const string CSI_RequestCursorPositionReport_Terminator = "R";
+ public static readonly AnsiEscapeSequenceRequest CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" };
///
/// ESC [ 0 c - Send Device Attributes (Primary DA)
@@ -1341,20 +1823,18 @@ public enum DECSCUSR_Style
/// 28 = Rectangular area operations
/// 32 = Text macros
/// 42 = ISO Latin-2 character set
+ /// The terminator indicating a reply to or
+ ///
///
- public static readonly string CSI_SendDeviceAttributes = CSI + "0c";
+ public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes = new () { Request = CSI + "0c", Terminator = "c" };
///
/// ESC [ > 0 c - Send Device Attributes (Secondary DA)
/// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220)
- ///
- public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c";
-
- ///
/// The terminator indicating a reply to or
///
///
- public const string CSI_ReportDeviceAttributes_Terminator = "c";
+ public static readonly AnsiEscapeSequenceRequest CSI_SendDeviceAttributes2 = new () { Request = CSI + ">0c", Terminator = "c" };
/*
TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
@@ -1372,19 +1852,9 @@ public enum DECSCUSR_Style
///
/// CSI 1 8 t | yes | yes | yes | report window size in chars
/// https://terminalguide.namepad.de/seq/csi_st-18/
- ///
- public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t";
-
- ///
/// The terminator indicating a reply to : ESC [ 8 ; height ; width t
///
- public const string CSI_ReportTerminalSizeInChars_Terminator = "t";
-
- ///
- /// The value of the response to indicating value 1 and 2 are the terminal
- /// size in chars.
- ///
- public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8";
+ public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", ExpectedResponseValue = "8" };
#endregion
}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequests.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequests.cs
new file mode 100644
index 0000000000..62c0908fbc
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequests.cs
@@ -0,0 +1,80 @@
+#nullable enable
+
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+///
+/// Manages ANSI Escape Sequence requests and responses. The list of
+/// contains the
+/// status of the request. Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator).
+///
+public static class AnsiEscapeSequenceRequests
+{
+ ///
+ /// Adds a new request for the ANSI Escape Sequence defined by . Adds a
+ /// instance to list.
+ ///
+ /// The object.
+ public static void Add (AnsiEscapeSequenceRequest ansiRequest)
+ {
+ lock (ansiRequest._responseLock)
+ {
+ Statuses.Enqueue (new (ansiRequest));
+ }
+
+ System.Diagnostics.Debug.Assert (Statuses.Count > 0);
+ }
+
+ ///
+ /// Clear the property.
+ ///
+ public static void Clear ()
+ {
+ lock (Statuses)
+ {
+ Statuses.Clear ();
+ }
+ }
+
+ ///
+ /// Indicates if a with the exists in the
+ /// list.
+ ///
+ ///
+ ///
+ /// if exist, otherwise.
+ public static bool HasResponse (string terminator, out AnsiEscapeSequenceRequestStatus? seqReqStatus)
+ {
+ lock (Statuses)
+ {
+ Statuses.TryPeek (out seqReqStatus);
+
+ return seqReqStatus?.AnsiRequest.Terminator == terminator;
+ }
+ }
+
+ ///
+ /// Removes a request defined by . If a matching
+ /// is
+ /// found and the number of outstanding requests is greater than 0, the number of outstanding requests is decremented.
+ /// If the number of outstanding requests is 0, the is removed from
+ /// .
+ ///
+ /// The object.
+ public static void Remove (AnsiEscapeSequenceRequestStatus? seqReqStatus)
+ {
+ lock (Statuses)
+ {
+ Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus? request);
+
+ if (request != seqReqStatus)
+ {
+ throw new InvalidOperationException ("Both EscSeqReqStatus objects aren't equals.");
+ }
+ }
+ }
+
+ /// Gets the list.
+ public static ConcurrentQueue Statuses { get; } = new ();
+}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
new file mode 100644
index 0000000000..104cd760ac
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
@@ -0,0 +1,64 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Describes a response received from the console as a result of a request being sent via
+/// .
+///
+public class AnsiEscapeSequenceResponse
+{
+ ///
+ /// Gets the error string received from e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// .
+ ///
+ public required string Error { get; init; }
+
+ ///
+ /// The value expected in the response after the CSI e.g.
+ ///
+ /// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
+ ///
+ /// should result in a response of the form ESC [ 8 ; height ; width t. In this case,
+ ///
+ /// will be "8".
+ ///
+
+ public string? ExpectedResponseValue { get; init; }
+
+ ///
+ /// Gets the Response string received from e.g. see
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// .
+ ///
+ public required string? Response { get; init; }
+
+ ///
+ ///
+ /// Gets the terminator that uniquely identifies the response received from
+ /// the console. e.g. for
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Request
+ ///
+ /// the terminator is
+ ///
+ /// EscSeqUtils.CSI_SendDeviceAttributes.Terminator
+ ///
+ /// .
+ ///
+ ///
+ /// After sending a request, the first response with matching terminator will be matched
+ /// to the oldest outstanding request.
+ ///
+ ///
+ public required string Terminator { get; init; }
+
+ ///
+ /// Gets if the request has a valid response.
+ ///
+ public bool Valid { get; internal set; }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index e1e1730011..d7ecb9ac73 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -1,8 +1,6 @@
#nullable enable
-//
-// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
-//
+using System.Collections.Concurrent;
using System.Diagnostics;
namespace Terminal.Gui;
@@ -15,6 +13,162 @@ namespace Terminal.Gui;
///
public abstract class ConsoleDriver
{
+ private readonly ManualResetEventSlim _waitAnsiRequest = new (false);
+ private readonly ManualResetEventSlim _ansiResponseReady = new (false);
+ private readonly CancellationTokenSource? _ansiRequestTokenSource = new ();
+ private readonly ConcurrentQueue _requestQueue = new ();
+ private readonly ConcurrentQueue _responseQueue = new ();
+ private IMainLoopDriver? _mainLoopDriver;
+
+ internal void ProcessAnsiRequestHandler ()
+ {
+ while (_ansiRequestTokenSource is { IsCancellationRequested: false})
+ {
+ try
+ {
+ if (_requestQueue.Count == 0)
+ {
+ try
+ {
+ _waitAnsiRequest.Wait (_ansiRequestTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ _waitAnsiRequest.Reset ();
+ }
+
+ while (_requestQueue.TryDequeue (out AnsiEscapeSequenceRequest? ansiRequest))
+ {
+ try
+ {
+ lock (ansiRequest._responseLock)
+ {
+ AnsiEscapeSequenceRequest request = ansiRequest;
+
+ ansiRequest.ResponseFromInput += (s, e) =>
+ {
+ Debug.Assert (s == request);
+ Debug.Assert (e == request.AnsiEscapeSequenceResponse);
+
+ _responseQueue.Enqueue (request);
+
+ _ansiResponseReady.Set ();
+ };
+
+ AnsiEscapeSequenceRequests.Add (ansiRequest);
+
+ WriteRaw (ansiRequest.Request);
+
+ _mainLoopDriver!.ForceRead = true;
+ }
+
+ if (!_ansiRequestTokenSource.IsCancellationRequested)
+ {
+ _mainLoopDriver.WaitForInput.Set ();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
+ ///
+ /// public ColorTests ()
+ /// {
+ /// ConsoleDriver.RunningUnitTests = true;
+ /// }
+ ///
+ ///
+ internal static bool RunningUnitTests { get; set; }
+
+ /// Get the operating system clipboard.
+ public IClipboard? Clipboard { get; internal set; }
+
+ /// Returns the name of the driver and relevant library version information.
+ ///
+ public virtual string GetVersionInfo () { return GetType ().Name; }
+
+ #region ANSI Esc Sequence Handling
+
+ ///
+ /// Provide unique handling for all the terminal write ANSI escape sequence request.
+ ///
+ /// The object.
+ /// The object.
+ /// if the request response is valid, otherwise.
+ public bool TryWriteAnsiRequest (IMainLoopDriver mainLoopDriver, ref AnsiEscapeSequenceRequest ansiRequest)
+ {
+ ArgumentNullException.ThrowIfNull (mainLoopDriver, nameof (mainLoopDriver));
+ ArgumentNullException.ThrowIfNull (ansiRequest, nameof (ansiRequest));
+
+ lock (ansiRequest._responseLock)
+ {
+ _mainLoopDriver = mainLoopDriver;
+ _requestQueue.Enqueue (ansiRequest);
+
+ _waitAnsiRequest.Set ();
+ }
+
+ try
+ {
+ _ansiResponseReady.Wait (_ansiRequestTokenSource!.Token);
+
+ _ansiResponseReady.Reset ();
+
+ _responseQueue.TryDequeue (out _);
+
+ lock (ansiRequest._responseLock)
+ {
+ _mainLoopDriver.ForceRead = false;
+
+ if (AnsiEscapeSequenceRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? request))
+ {
+ if (AnsiEscapeSequenceRequests.Statuses.Count > 0
+ && string.IsNullOrEmpty (request.AnsiRequest.AnsiEscapeSequenceResponse?.Response))
+ {
+ lock (request.AnsiRequest._responseLock)
+ {
+ // Bad request or no response at all
+ AnsiEscapeSequenceRequests.Statuses.TryDequeue (out _);
+ }
+ }
+ }
+
+ return ansiRequest.AnsiEscapeSequenceResponse is { Valid: true };
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ }
+
+ // 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?
+ ///
+ /// Provide proper writing to send escape sequence recognized by the .
+ ///
+ ///
+ internal 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,7 +177,7 @@ public abstract class ConsoleDriver
/// Gets the location and size of the terminal screen.
internal Rectangle Screen => new (0, 0, Cols, Rows);
- private Region? _clip = null;
+ private Region? _clip;
///
/// Gets or sets the clip rectangle that and are subject
@@ -50,9 +204,6 @@ internal Region? Clip
}
}
- /// Get the operating system clipboard.
- public IClipboard? Clipboard { get; internal set; }
-
///
/// Gets the column last set by . and are used by
/// and to determine where to add content.
@@ -80,6 +231,43 @@ internal virtual int Cols
/// The leftmost column in the terminal.
internal virtual int Left { get; set; } = 0;
+ /// Tests if the specified rune is supported by the driver.
+ ///
+ ///
+ /// if the rune can be properly presented; if the driver does not
+ /// support displaying this rune.
+ ///
+ public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
+
+ /// Tests whether the specified coordinate are valid for drawing.
+ /// The column.
+ /// The row.
+ ///
+ /// if the coordinate is outside the screen bounds or outside of .
+ /// otherwise.
+ ///
+ public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); }
+
+ ///
+ /// Updates and to the specified column and row in .
+ /// Used by and to determine where to add content.
+ ///
+ ///
+ /// This does not move the cursor on the screen, it only updates the internal state of the driver.
+ ///
+ /// If or are negative or beyond and
+ /// , the method still sets those properties.
+ ///
+ ///
+ /// Column to move to.
+ /// Row to move to.
+ 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;
+ }
+
///
/// Gets the row last set by . and are used by
/// and to determine where to add content.
@@ -100,17 +288,6 @@ internal virtual int Rows
/// The topmost row in the terminal.
internal virtual int Top { get; set; } = 0;
- ///
- /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
- ///
- /// public ColorTests ()
- /// {
- /// ConsoleDriver.RunningUnitTests = true;
- /// }
- ///
- ///
- internal static bool RunningUnitTests { get; set; }
-
/// Adds the specified rune to the display at the current cursor position.
///
///
@@ -151,7 +328,7 @@ internal void AddRune (Rune rune)
// 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 `[é ]`.
//
- // Until this is addressed (see Issue #), we do our best by
+ // Until this is addressed (see Issue #), we do our best by
// a) Attempting to normalize any CM with the base char to it's left
// b) Ignoring any CMs that don't normalize
if (Col > 0)
@@ -174,7 +351,7 @@ internal void AddRune (Rune rune)
if (normalized.Length == 1)
{
// It normalized! We can just set the Cell to the left with the
- // normalized codepoint
+ // normalized codepoint
Contents [Row, Col - 1].Rune = (Rune)normalized [0];
// Ignore. Don't move to next column because we're already there
@@ -322,6 +499,37 @@ internal void AddStr (string str)
}
}
+ /// Fills the specified rectangle with the specified rune, using
+ ///
+ /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn.
+ ///
+ /// The Screen-relative rectangle.
+ /// The Rune used to fill the rectangle
+ internal void FillRect (Rectangle rect, Rune rune = default)
+ {
+ // BUGBUG: This should be a method on Region
+ rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
+ lock (Contents!)
+ {
+ for (int r = rect.Y; r < rect.Y + rect.Height; r++)
+ {
+ for (int c = rect.X; c < rect.X + rect.Width; c++)
+ {
+ if (!IsValidLocation (rune, c, r))
+ {
+ continue;
+ }
+ Contents [r, c] = new Cell
+ {
+ Rune = rune != default ? rune : (Rune)' ',
+ Attribute = CurrentAttribute, IsDirty = true
+ };
+ _dirtyLines! [r] = true;
+ }
+ }
+ }
+ }
+
/// Clears the of the driver.
internal void ClearContents ()
{
@@ -339,13 +547,14 @@ internal void ClearContents ()
{
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;
}
}
@@ -372,42 +581,8 @@ internal void SetContentsAsDirty ()
{
Contents [row, c].IsDirty = true;
}
- _dirtyLines! [row] = true;
- }
- }
- }
-
- /// Determines if the terminal cursor should be visible or not and sets it accordingly.
- /// upon success
- public abstract bool EnsureCursorVisibility ();
- /// Fills the specified rectangle with the specified rune, using
- ///
- /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn.
- ///
- /// The Screen-relative rectangle.
- /// The Rune used to fill the rectangle
- internal void FillRect (Rectangle rect, Rune rune = default)
- {
- // BUGBUG: This should be a method on Region
- rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
- lock (Contents!)
- {
- for (int r = rect.Y; r < rect.Y + rect.Height; r++)
- {
- for (int c = rect.X; c < rect.X + rect.Width; c++)
- {
- if (!IsValidLocation (rune, c, r))
- {
- continue;
- }
- Contents [r, c] = new Cell
- {
- Rune = (rune != default ? rune : (Rune)' '),
- Attribute = CurrentAttribute, IsDirty = true
- };
- _dirtyLines! [r] = true;
- }
+ _dirtyLines! [row] = true;
}
}
}
@@ -420,23 +595,19 @@ internal void FillRect (Rectangle rect, Rune rune = default)
///
internal void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); }
+ #endregion Screen and Contents
+
+ #region Cursor Handling
+
+ /// Determines if the terminal cursor should be visible or not and sets it accordingly.
+ /// upon success
+ public abstract bool EnsureCursorVisibility ();
+
/// Gets the terminal cursor visibility.
/// The current
/// upon success
public abstract bool GetCursorVisibility (out CursorVisibility visibility);
- /// Returns the name of the driver and relevant library version information.
- ///
- public virtual string GetVersionInfo () { return GetType ().Name; }
-
- /// Tests if the specified rune is supported by the driver.
- ///
- ///
- /// if the rune can be properly presented; if the driver does not
- /// support displaying this rune.
- ///
- public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); }
-
/// Tests whether the specified coordinate are valid for drawing the specified Rune.
/// Used to determine if one or two columns are required.
/// The column.
@@ -458,27 +629,6 @@ internal bool IsValidLocation (Rune rune, int col, int row)
}
}
- // TODO: Make internal once Menu is upgraded
- ///
- /// Updates and to the specified column and row in .
- /// Used by and to determine where to add content.
- ///
- ///
- /// This does not move the cursor on the screen, it only updates the internal state of the driver.
- ///
- /// If or are negative or beyond and
- /// , the method still sets those properties.
- ///
- ///
- /// Column to move to.
- /// Row to move to.
- 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;
- }
-
/// Called when the terminal size changes. Fires the event.
///
internal void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
@@ -505,6 +655,8 @@ internal void Refresh ()
/// The event fired when the terminal is resized.
public event EventHandler? SizeChanged;
+ #endregion Cursor Handling
+
/// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.
/// This is only implemented in .
public abstract void Suspend ();
@@ -566,7 +718,7 @@ public Attribute CurrentAttribute
// 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;
}
@@ -599,16 +751,33 @@ internal Attribute SetAttribute (Attribute c)
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 Handling
+
+ /// Event fired when a mouse event occurs.
+ public event EventHandler? MouseEvent;
+
+ /// Called when a mouse event occurs. Fires the event.
+ ///
+ public void OnMouseEvent (MouseEventArgs a)
+ {
+ // Ensure ScreenPosition is set
+ a.ScreenPosition = a.Position;
+
+ MouseEvent?.Invoke (this, a);
+ }
- #region Mouse and Keyboard
+ #endregion Mouse Handling
+
+ #region Keyboard Handling
/// Event fired when a key is pressed down. This is a precursor to .
public event EventHandler? KeyDown;
@@ -635,19 +804,8 @@ public virtual Attribute MakeColor (in Color foreground, in Color background)
///
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
- /// Event fired when a mouse event occurs.
- public event EventHandler? MouseEvent;
-
- /// Called when a mouse event occurs. Fires the event.
- ///
- 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
/// Simulates a key press.
/// The key character.
/// The key.
@@ -656,324 +814,5 @@ public void OnMouseEvent (MouseEventArgs a)
/// If simulates the Ctrl key being pressed.
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
- #endregion
-}
-
-///
-/// The enumeration encodes key information from s and provides a
-/// consistent way for application code to specify keys and receive key events.
-///
-/// The class provides a higher-level abstraction, with helper methods and properties for
-/// common operations. For example, and provide a convenient way
-/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
-///
-///
-///
-///
-/// 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. , , etc.).
-/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent
-/// *lowercase*, un-shifted characters.
-///
-///
-/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. ,
-/// , etc.).
-///
-///
-/// The shift modifiers (, , and
-/// ) can be combined (with logical or) with the other key codes to represent shifted
-/// keys. For example, the enum value represents the un-shifted 'a' key, while
-/// | represents the 'A' key (shifted 'a' key). Likewise,
-/// | represents the 'Alt+A' key combination.
-///
-///
-/// All other keys that produce a printable character are encoded as the Unicode value of the character. For
-/// example, the for the '!' character is 33, which is the Unicode value for '!'. Likewise,
-/// `â` is 226, `Â` is 194, etc.
-///
-///
-/// If the is set, then the value is that of the special mask, otherwise, the value is
-/// the one of the lower bits (as extracted by ).
-///
-///
-[Flags]
-public enum KeyCode : uint
-{
- ///
- /// 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.
- ///
- CharMask = 0x_f_ffff,
-
- ///
- /// If the is set, then the value is that of the special mask, otherwise, the value is
- /// in the lower bits (as extracted by ).
- ///
- SpecialMask = 0x_fff0_0000,
-
- ///
- /// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by
- /// removing the ShiftMask.
- ///
- ShiftMask = 0x_1000_0000,
-
- ///
- /// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by
- /// removing the AltMask.
- ///
- AltMask = 0x_8000_0000,
-
- ///
- /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by
- /// removing the CtrlMask.
- ///
- CtrlMask = 0x_4000_0000,
-
- /// The key code representing an invalid or empty key.
- Null = 0,
-
- /// Backspace key.
- Backspace = 8,
-
- /// The key code for the tab key (forwards tab key).
- Tab = 9,
-
- /// The key code for the return key.
- Enter = ConsoleKey.Enter,
-
- /// The key code for the clear key.
- Clear = 12,
-
- /// The key code for the escape key.
- Esc = 27,
-
- /// The key code for the space bar key.
- Space = 32,
-
- /// Digit 0.
- D0 = 48,
-
- /// Digit 1.
- D1,
-
- /// Digit 2.
- D2,
-
- /// Digit 3.
- D3,
-
- /// Digit 4.
- D4,
-
- /// Digit 5.
- D5,
-
- /// Digit 6.
- D6,
-
- /// Digit 7.
- D7,
-
- /// Digit 8.
- D8,
-
- /// Digit 9.
- D9,
-
- /// The key code for the A key
- A = 65,
-
- /// The key code for the B key
- B,
-
- /// The key code for the C key
- C,
-
- /// The key code for the D key
- D,
-
- /// The key code for the E key
- E,
-
- /// The key code for the F key
- F,
-
- /// The key code for the G key
- G,
-
- /// The key code for the H key
- H,
-
- /// The key code for the I key
- I,
-
- /// The key code for the J key
- J,
-
- /// The key code for the K key
- K,
-
- /// The key code for the L key
- L,
-
- /// The key code for the M key
- M,
-
- /// The key code for the N key
- N,
-
- /// The key code for the O key
- O,
-
- /// The key code for the P key
- P,
-
- /// The key code for the Q key
- Q,
-
- /// The key code for the R key
- R,
-
- /// The key code for the S key
- S,
-
- /// The key code for the T key
- T,
-
- /// The key code for the U key
- U,
-
- /// The key code for the V key
- V,
-
- /// The key code for the W key
- W,
-
- /// The key code for the X key
- X,
-
- /// The key code for the Y key
- Y,
-
- /// The key code for the Z key
- Z,
-
- /////
- ///// The key code for the Delete key.
- /////
- //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.
-
- /// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys.
- MaxCodePoint = 0x10FFFF,
-
- /// Cursor up key
- CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
-
- /// Cursor down key.
- CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
-
- /// Cursor left key.
- CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
-
- /// Cursor right key.
- CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
-
- /// Page Up key.
- PageUp = MaxCodePoint + ConsoleKey.PageUp,
-
- /// Page Down key.
- PageDown = MaxCodePoint + ConsoleKey.PageDown,
-
- /// Home key.
- Home = MaxCodePoint + ConsoleKey.Home,
-
- /// End key.
- End = MaxCodePoint + ConsoleKey.End,
-
- /// Insert (INS) key.
- Insert = MaxCodePoint + ConsoleKey.Insert,
-
- /// Delete (DEL) key.
- Delete = MaxCodePoint + ConsoleKey.Delete,
-
- /// Print screen character key.
- PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
-
- /// F1 key.
- F1 = MaxCodePoint + ConsoleKey.F1,
-
- /// F2 key.
- F2 = MaxCodePoint + ConsoleKey.F2,
-
- /// F3 key.
- F3 = MaxCodePoint + ConsoleKey.F3,
-
- /// F4 key.
- F4 = MaxCodePoint + ConsoleKey.F4,
-
- /// F5 key.
- F5 = MaxCodePoint + ConsoleKey.F5,
-
- /// F6 key.
- F6 = MaxCodePoint + ConsoleKey.F6,
-
- /// F7 key.
- F7 = MaxCodePoint + ConsoleKey.F7,
-
- /// F8 key.
- F8 = MaxCodePoint + ConsoleKey.F8,
-
- /// F9 key.
- F9 = MaxCodePoint + ConsoleKey.F9,
-
- /// F10 key.
- F10 = MaxCodePoint + ConsoleKey.F10,
-
- /// F11 key.
- F11 = MaxCodePoint + ConsoleKey.F11,
-
- /// F12 key.
- F12 = MaxCodePoint + ConsoleKey.F12,
-
- /// F13 key.
- F13 = MaxCodePoint + ConsoleKey.F13,
-
- /// F14 key.
- F14 = MaxCodePoint + ConsoleKey.F14,
-
- /// F15 key.
- F15 = MaxCodePoint + ConsoleKey.F15,
-
- /// F16 key.
- F16 = MaxCodePoint + ConsoleKey.F16,
-
- /// F17 key.
- F17 = MaxCodePoint + ConsoleKey.F17,
-
- /// F18 key.
- F18 = MaxCodePoint + ConsoleKey.F18,
-
- /// F19 key.
- F19 = MaxCodePoint + ConsoleKey.F19,
-
- /// F20 key.
- F20 = MaxCodePoint + ConsoleKey.F20,
-
- /// F21 key.
- F21 = MaxCodePoint + ConsoleKey.F21,
-
- /// F22 key.
- F22 = MaxCodePoint + ConsoleKey.F22,
-
- /// F23 key.
- F23 = MaxCodePoint + ConsoleKey.F23,
-
- /// F24 key.
- F24 = MaxCodePoint + ConsoleKey.F24
+ #endregion Keyboard Handling
}
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
index c3497f3acd..6fce2e040f 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
@@ -3,6 +3,7 @@
namespace Terminal.Gui.ConsoleDrivers;
+// QUESTION: This class combines Windows specific code with cross-platform code. Should this be split into two classes?
/// Helper class to handle the scan code and virtual key from a .
public static class ConsoleKeyMapping
{
@@ -249,7 +250,7 @@ public static ConsoleModifiers MapToConsoleModifiers (KeyCode key)
{
var modifiers = new ConsoleModifiers ();
- if (key.HasFlag (KeyCode.ShiftMask))
+ if (key.HasFlag (KeyCode.ShiftMask) || char.IsUpper ((char)key))
{
modifiers |= ConsoleModifiers.Shift;
}
@@ -590,7 +591,8 @@ internal static uint GetKeyCharFromUnicodeChar (
if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter)
{
- consoleKey = char.ToUpper (stFormD [i]);
+ char ck = char.ToUpper (stFormD [i]);
+ consoleKey = (uint)(ck > 0 && ck <= 255 ? char.ToUpper (stFormD [i]) : 0);
scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0);
if (scode is { })
@@ -704,6 +706,32 @@ internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsol
return (uint)ConsoleKey.F24;
case KeyCode.Tab | KeyCode.ShiftMask:
return (uint)ConsoleKey.Tab;
+ case KeyCode.Space:
+ return (uint)ConsoleKey.Spacebar;
+ default:
+ uint c = (char)keyValue;
+
+ if (c is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
+ {
+ return c;
+ }
+
+ if ((c - 32) is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z)
+ {
+ return (c - 32);
+ }
+
+ if (Enum.IsDefined (typeof (ConsoleKey), keyValue.ToString ()))
+ {
+ return (uint)keyValue;
+ }
+
+ // DEL
+ if ((uint)keyValue == 127)
+ {
+ return (uint)ConsoleKey.Backspace;
+ }
+ break;
}
isConsoleKey = false;
@@ -867,6 +895,14 @@ public static KeyCode MapConsoleKeyInfoToKeyCode (ConsoleKeyInfo consoleKeyInfo)
case ConsoleKey.Tab:
keyCode = KeyCode.Tab;
+ break;
+ case ConsoleKey.Spacebar:
+ keyCode = KeyCode.Space;
+
+ break;
+ case ConsoleKey.Backspace:
+ keyCode = KeyCode.Backspace;
+
break;
default:
if ((int)consoleKeyInfo.KeyChar is >= 1 and <= 26)
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 446926fef1..98cdbdba9e 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -1,23 +1,18 @@
-//
+#nullable enable
+//
// Driver.cs: Curses-based Driver
//
-using System.Diagnostics;
using System.Runtime.InteropServices;
using Terminal.Gui.ConsoleDrivers;
using Unix.Terminal;
namespace Terminal.Gui;
-/// This is the Curses driver for the gui.cs/Terminal framework.
+/// A Linux/Mac driver based on the Curses library.
internal class CursesDriver : ConsoleDriver
{
- public Curses.Window _window;
- private CursorVisibility? _currentCursorVisibility;
- private CursorVisibility? _initialCursorVisibility;
- private MouseFlags _lastMouseFlags;
- private UnixMainLoop _mainLoopDriver;
- private object _processInputToken;
+ public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
internal override int Cols
{
@@ -39,45 +34,6 @@ internal override int Rows
}
}
- public override bool SupportsTrueColor => true;
-
- ///
- public override bool EnsureCursorVisibility () { return false; }
-
- ///
- public override bool GetCursorVisibility (out CursorVisibility visibility)
- {
- visibility = CursorVisibility.Invisible;
-
- if (!_currentCursorVisibility.HasValue)
- {
- return false;
- }
-
- visibility = _currentCursorVisibility.Value;
-
- return true;
- }
-
- public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
-
- public static bool Is_WSL_Platform ()
- {
- // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
- //if (new CursesClipboard ().IsSupported) {
- // // If xclip is installed on Linux under WSL, this will return true.
- // return false;
- //}
- (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
-
- if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
- {
- return true;
- }
-
- return false;
- }
-
public override bool IsRuneSupported (Rune rune)
{
// See Issue #2615 - CursesDriver is broken with non-BMP characters
@@ -101,34 +57,33 @@ public override void Move (int col, int row)
{
// Not a valid location (outside screen or clip region)
// Move within the clip region, then AddRune will actually move to Col, Row
- Rectangle clipRect = Clip.GetBounds ();
+ Rectangle clipRect = Clip!.GetBounds ();
Curses.move (clipRect.Y, clipRect.X);
}
}
-
public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
{
KeyCode key;
if (consoleKey == ConsoleKey.Packet)
{
- var mod = new ConsoleModifiers ();
+ //var mod = new ConsoleModifiers ();
- if (shift)
- {
- mod |= ConsoleModifiers.Shift;
- }
+ //if (shift)
+ //{
+ // mod |= ConsoleModifiers.Shift;
+ //}
- if (alt)
- {
- mod |= ConsoleModifiers.Alt;
- }
+ //if (alt)
+ //{
+ // mod |= ConsoleModifiers.Alt;
+ //}
- if (control)
- {
- mod |= ConsoleModifiers.Control;
- }
+ //if (control)
+ //{
+ // mod |= ConsoleModifiers.Control;
+ //}
var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
@@ -139,45 +94,17 @@ public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift,
key = (KeyCode)keyChar;
}
- OnKeyDown (new Key (key));
- OnKeyUp (new Key (key));
+ OnKeyDown (new (key));
+ OnKeyUp (new (key));
//OnKeyPressed (new KeyEventArgsEventArgs (key));
}
- ///
- public override bool SetCursorVisibility (CursorVisibility visibility)
- {
- if (_initialCursorVisibility.HasValue == false)
- {
- return false;
- }
-
- if (!RunningUnitTests)
- {
- Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
- }
-
- if (visibility != CursorVisibility.Invisible)
- {
- Console.Out.Write (
- EscSeqUtils.CSI_SetCursorStyle (
- (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24)
- & 0xFF)
- )
- );
- }
-
- _currentCursorVisibility = visibility;
-
- return true;
- }
-
public void StartReportingMouseMoves ()
{
if (!RunningUnitTests)
{
- Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_EnableMouseEvents);
}
}
@@ -185,7 +112,7 @@ public void StopReportingMouseMoves ()
{
if (!RunningUnitTests)
{
- Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_DisableMouseEvents);
}
}
@@ -213,14 +140,18 @@ public override void UpdateCursor ()
if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
{
- Curses.move (Row, Col);
-
if (Force16Colors)
{
+ Curses.move (Row, Col);
+
Curses.raw ();
Curses.noecho ();
Curses.refresh ();
}
+ else
+ {
+ _mainLoopDriver?.WriteRaw (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
+ }
}
}
@@ -231,7 +162,7 @@ public override bool UpdateScreen ()
{
for (var row = 0; row < Rows; row++)
{
- if (!_dirtyLines [row])
+ if (!_dirtyLines! [row])
{
continue;
}
@@ -240,7 +171,7 @@ public override bool UpdateScreen ()
for (var col = 0; col < Cols; col++)
{
- if (Contents [row, col].IsDirty == false)
+ if (Contents! [row, col].IsDirty == false)
{
continue;
}
@@ -284,14 +215,14 @@ public override bool UpdateScreen ()
if (!RunningUnitTests)
{
Curses.move (Row, Col);
- _window.wrefresh ();
+ _window?.wrefresh ();
}
}
else
{
if (RunningUnitTests
|| Console.WindowHeight < 1
- || Contents.Length != Rows * Cols
+ || Contents!.Length != Rows * Cols
|| Rows != Console.WindowHeight)
{
return updated;
@@ -315,7 +246,7 @@ public override bool UpdateScreen ()
return updated;
}
- if (!_dirtyLines [row])
+ if (!_dirtyLines! [row])
{
continue;
}
@@ -360,7 +291,7 @@ public override bool UpdateScreen ()
lastCol = col;
}
- Attribute attr = Contents [row, col].Attribute.Value;
+ Attribute attr = Contents [row, col].Attribute!.Value;
// Performance: Only send the escape sequence if the attribute has changed.
if (attr != redrawAttr)
@@ -368,7 +299,7 @@ public override bool UpdateScreen ()
redrawAttr = attr;
output.Append (
- EscSeqUtils.CSI_SetForegroundColorRGB (
+ AnsiEscapeSequenceRequestUtils.CSI_SetForegroundColorRGB (
attr.Foreground.R,
attr.Foreground.G,
attr.Foreground.B
@@ -376,7 +307,7 @@ public override bool UpdateScreen ()
);
output.Append (
- EscSeqUtils.CSI_SetBackgroundColorRGB (
+ AnsiEscapeSequenceRequestUtils.CSI_SetBackgroundColorRGB (
attr.Background.R,
attr.Background.G,
attr.Background.B
@@ -419,10 +350,10 @@ public override bool UpdateScreen ()
}
// SIXELS
- foreach (var s in Application.Sixel)
+ foreach (SixelToRender s in Application.Sixel)
{
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
- Console.Write(s.SixelData);
+ Console.Write (s.SixelData);
}
SetCursorPosition (0, 0);
@@ -442,40 +373,217 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out
return updated;
}
- private bool SetCursorPosition (int col, int row)
+ #region Color Handling
+
+ public override bool SupportsTrueColor => true;
+
+ /// Creates an Attribute from the provided curses-based foreground and background color numbers
+ /// Contains the curses color number for the foreground (color, plus any attributes)
+ /// Contains the curses color number for the background (color, plus any attributes)
+ ///
+ private static Attribute MakeColor (short foreground, short background)
{
- // + 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));
+ //var v = (short)((ushort)foreground | (background << 4));
+ var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
+
+ // TODO: for TrueColor - Use InitExtendedPair
+ Curses.InitColorPair (v, foreground, background);
+
+ return new (
+ Curses.ColorPair (v),
+ CursesColorNumberToColorName16 (foreground),
+ CursesColorNumberToColorName16 (background)
+ );
+ }
+
+ ///
+ ///
+ /// In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4
+ /// bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are
+ /// converted to curses color encoding before being encoded.
+ ///
+ public override Attribute MakeColor (in Color foreground, in Color background)
+ {
+ if (!RunningUnitTests && Force16Colors)
+ {
+ return MakeColor (
+ ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
+ ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
+ );
+ }
+
+ return new (
+ 0,
+ foreground,
+ background
+ );
+ }
+
+ private static short ColorNameToCursesColorNumber (ColorName16 color)
+ {
+ switch (color)
+ {
+ case ColorName16.Black:
+ return Curses.COLOR_BLACK;
+ case ColorName16.Blue:
+ return Curses.COLOR_BLUE;
+ case ColorName16.Green:
+ return Curses.COLOR_GREEN;
+ case ColorName16.Cyan:
+ return Curses.COLOR_CYAN;
+ case ColorName16.Red:
+ return Curses.COLOR_RED;
+ case ColorName16.Magenta:
+ return Curses.COLOR_MAGENTA;
+ case ColorName16.Yellow:
+ return Curses.COLOR_YELLOW;
+ case ColorName16.Gray:
+ return Curses.COLOR_WHITE;
+ case ColorName16.DarkGray:
+ return Curses.COLOR_GRAY;
+ case ColorName16.BrightBlue:
+ return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
+ case ColorName16.BrightGreen:
+ return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
+ case ColorName16.BrightCyan:
+ return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
+ case ColorName16.BrightRed:
+ return Curses.COLOR_RED | Curses.COLOR_GRAY;
+ case ColorName16.BrightMagenta:
+ return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
+ case ColorName16.BrightYellow:
+ return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
+ case ColorName16.White:
+ return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
+ }
+
+ throw new ArgumentException ("Invalid color code");
+ }
+
+ private static ColorName16 CursesColorNumberToColorName16 (short color)
+ {
+ switch (color)
+ {
+ case Curses.COLOR_BLACK:
+ return ColorName16.Black;
+ case Curses.COLOR_BLUE:
+ return ColorName16.Blue;
+ case Curses.COLOR_GREEN:
+ return ColorName16.Green;
+ case Curses.COLOR_CYAN:
+ return ColorName16.Cyan;
+ case Curses.COLOR_RED:
+ return ColorName16.Red;
+ case Curses.COLOR_MAGENTA:
+ return ColorName16.Magenta;
+ case Curses.COLOR_YELLOW:
+ return ColorName16.Yellow;
+ case Curses.COLOR_WHITE:
+ return ColorName16.Gray;
+ case Curses.COLOR_GRAY:
+ return ColorName16.DarkGray;
+ case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
+ return ColorName16.BrightBlue;
+ case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
+ return ColorName16.BrightGreen;
+ case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
+ return ColorName16.BrightCyan;
+ case Curses.COLOR_RED | Curses.COLOR_GRAY:
+ return ColorName16.BrightRed;
+ case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
+ return ColorName16.BrightMagenta;
+ case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
+ return ColorName16.BrightYellow;
+ case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
+ return ColorName16.White;
+ }
+
+ throw new ArgumentException ("Invalid curses color code");
+ }
+
+ #endregion
+
+ private CursorVisibility? _currentCursorVisibility;
+ private CursorVisibility? _initialCursorVisibility;
+
+ ///
+ public override bool EnsureCursorVisibility ()
+ {
+ if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+ {
+ GetCursorVisibility (out CursorVisibility cursorVisibility);
+ _currentCursorVisibility = cursorVisibility;
+ SetCursorVisibility (CursorVisibility.Invisible);
+
+ return false;
+ }
+
+ SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
+
+ return _currentCursorVisibility == CursorVisibility.Default;
+ }
+
+ ///
+ public override bool GetCursorVisibility (out CursorVisibility visibility)
+ {
+ visibility = CursorVisibility.Invisible;
+
+ if (!_currentCursorVisibility.HasValue)
+ {
+ return false;
+ }
+
+ visibility = _currentCursorVisibility.Value;
return true;
}
- internal override void End ()
+ ///
+ public override bool SetCursorVisibility (CursorVisibility visibility)
{
- StopReportingMouseMoves ();
- SetCursorVisibility (CursorVisibility.Default);
+ if (_initialCursorVisibility.HasValue == false)
+ {
+ return false;
+ }
- if (_mainLoopDriver is { })
+ if (!RunningUnitTests)
{
- _mainLoopDriver.RemoveWatch (_processInputToken);
+ Curses.curs_set (((int)visibility >> 16) & 0x000000FF);
}
- if (RunningUnitTests)
+ if (visibility != CursorVisibility.Invisible)
{
- return;
+ _mainLoopDriver?.WriteRaw (
+ AnsiEscapeSequenceRequestUtils.CSI_SetCursorStyle (
+ (AnsiEscapeSequenceRequestUtils.DECSCUSR_Style)
+ (((int)visibility >> 24)
+ & 0xFF)
+ )
+ );
}
- // throws away any typeahead that has been typed by
- // the user and has not yet been read by the program.
- Curses.flushinp ();
+ _currentCursorVisibility = visibility;
- Curses.endwin ();
+ return true;
}
+ private bool SetCursorPosition (int col, int row)
+ {
+ // + 1 is needed because non-Windows is based on 1 instead of 0 and
+ // Console.CursorTop/CursorLeft isn't reliable.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (row + 1, col + 1));
+
+ return true;
+ }
+
+ #region Init/End/MainLoop
+
+ private Curses.Window? _window;
+ private UnixMainLoop? _mainLoopDriver;
+
internal override MainLoop Init ()
{
- _mainLoopDriver = new UnixMainLoop (this);
+ _mainLoopDriver = new (this);
if (!RunningUnitTests)
{
@@ -530,20 +638,9 @@ internal override MainLoop Init ()
{
Curses.timeout (0);
}
-
- _processInputToken = _mainLoopDriver?.AddWatch (
- 0,
- UnixMainLoop.Condition.PollIn,
- x =>
- {
- ProcessInput ();
-
- return true;
- }
- );
}
- CurrentAttribute = new Attribute (ColorName16.White, ColorName16.Black);
+ CurrentAttribute = new (ColorName16.White, ColorName16.Black);
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
@@ -574,578 +671,102 @@ internal override MainLoop Init ()
if (!RunningUnitTests)
{
Curses.CheckWinChange ();
+ ClearContents ();
if (Force16Colors)
{
Curses.refresh ();
}
+
+ Task.Run (ProcessAnsiRequestHandler);
}
- return new MainLoop (_mainLoopDriver);
+ return new (_mainLoopDriver);
}
- internal void ProcessInput ()
+ internal void ProcessInput (UnixMainLoop.PollData inputEvent)
{
- int wch;
- int code = Curses.get_wch (out wch);
-
- //System.Diagnostics.Debug.WriteLine ($"code: {code}; wch: {wch}");
- if (code == Curses.ERR)
- {
- return;
- }
-
- var k = KeyCode.Null;
-
- if (code == Curses.KEY_CODE_YES)
+ switch (inputEvent.EventType)
{
- while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize)
- {
- ProcessWinChange ();
- code = Curses.get_wch (out wch);
- }
+ case UnixMainLoop.EventType.Key:
+ ConsoleKeyInfo consoleKeyInfo = inputEvent.KeyEvent;
- if (wch == 0)
- {
- return;
- }
-
- if (wch == Curses.KeyMouse)
- {
- int wch2 = wch;
+ KeyCode map = AnsiEscapeSequenceRequestUtils.MapKey (consoleKeyInfo);
- while (wch2 == Curses.KeyMouse)
+ if (map == KeyCode.Null)
{
- Key kea = null;
-
- ConsoleKeyInfo [] cki =
- {
- new ((char)KeyCode.Esc, 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false)
- };
- code = 0;
- HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
- }
-
- return;
- }
-
- k = MapCursesKey (wch);
-
- if (wch >= 277 && wch <= 288)
- {
- // Shift+(F1 - F12)
- wch -= 12;
- k = KeyCode.ShiftMask | MapCursesKey (wch);
- }
- else if (wch >= 289 && wch <= 300)
- {
- // Ctrl+(F1 - F12)
- wch -= 24;
- k = KeyCode.CtrlMask | MapCursesKey (wch);
- }
- else if (wch >= 301 && wch <= 312)
- {
- // Ctrl+Shift+(F1 - F12)
- wch -= 36;
- k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch);
- }
- else if (wch >= 313 && wch <= 324)
- {
- // Alt+(F1 - F12)
- wch -= 48;
- k = KeyCode.AltMask | MapCursesKey (wch);
- }
- else if (wch >= 325 && wch <= 327)
- {
- // Shift+Alt+(F1 - F3)
- wch -= 60;
- k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch);
- }
-
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
-
- return;
- }
-
- // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
- if (wch == 27)
- {
- Curses.timeout (10);
-
- code = Curses.get_wch (out int wch2);
-
- if (code == Curses.KEY_CODE_YES)
- {
- k = KeyCode.AltMask | MapCursesKey (wch);
- }
-
- Key key = null;
-
- if (code == 0)
- {
- // The ESC-number handling, debatable.
- // Simulates the AltMask itself by pressing Alt + Space.
- // Needed for macOS
- if (wch2 == (int)KeyCode.Space)
- {
- k = KeyCode.AltMask | KeyCode.Space;
- }
- else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A
- && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z)
- {
- k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space));
- }
- else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64)
- {
- k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64));
- }
- else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9)
- {
- k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
- }
- else
- {
- ConsoleKeyInfo [] cki =
- [
- new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
- ];
- HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
-
- return;
+ break;
}
- //else if (wch2 == Curses.KeyCSI)
- //{
- // ConsoleKeyInfo [] cki =
- // {
- // new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
- // };
- // HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
-
- // return;
- //}
- //else
- //{
- // // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
- // if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
- // {
- // k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
- // }
-
- // if (wch2 == 0)
- // {
- // k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
- // }
- // //else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
- // //{
- // // k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
- // //}
- // else if (wch2 < 256)
- // {
- // k = (KeyCode)wch2; // | KeyCode.AltMask;
- // }
- // else
- // {
- // k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
- // }
- //}
-
- key = new Key (k);
- }
- else
- {
- key = Key.Esc;
- }
-
- OnKeyDown (key);
- OnKeyUp (key);
- }
- else if (wch == Curses.KeyTab)
- {
- k = MapCursesKey (wch);
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
- }
- else if (wch == 127)
- {
- // Backspace needed for macOS
- k = KeyCode.Backspace;
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
- }
- else
- {
- // Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
- k = (KeyCode)wch;
- if (wch == 0)
- {
- k = KeyCode.CtrlMask | KeyCode.Space;
- }
- else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64)
- {
- if ((KeyCode)(wch + 64) != KeyCode.J)
- {
- k = KeyCode.CtrlMask | (KeyCode)(wch + 64);
- }
- }
- else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
- {
- k = (KeyCode)wch | KeyCode.ShiftMask;
- }
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
- if (wch == '\n' || wch == '\r')
- {
- k = KeyCode.Enter;
- }
+ break;
+ case UnixMainLoop.EventType.Mouse:
+ var me = new MouseEventArgs { Position = inputEvent.MouseEvent.Position, Flags = inputEvent.MouseEvent.MouseFlags };
+ OnMouseEvent (me);
- // Strip the KeyCode.Space flag off if it's set
- //if (k != KeyCode.Space && k.HasFlag (KeyCode.Space))
- if (Key.GetIsKeyCodeAtoZ (k) && (k & KeyCode.Space) != 0)
- {
- k &= ~KeyCode.Space;
- }
+ break;
+ case UnixMainLoop.EventType.WindowSize:
+ Size size = new (inputEvent.WindowSizeEvent.Size.Width, inputEvent.WindowSizeEvent.Size.Height);
+ ProcessWinChange (size);
- OnKeyDown (new Key (k));
- OnKeyUp (new Key (k));
+ break;
+ default:
+ throw new ArgumentOutOfRangeException ();
}
}
-
- internal void ProcessWinChange ()
+ private void ProcessWinChange (Size size)
{
- if (!RunningUnitTests && Curses.CheckWinChange ())
+ if (!RunningUnitTests && Curses.ChangeWindowSize (size.Height, size.Width))
{
ClearContents ();
- OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+ OnSizeChanged (new (new (Cols, Rows)));
}
}
- private void HandleEscSeqResponse (
- ref int code,
- ref KeyCode k,
- ref int wch2,
- ref Key keyEventArgs,
- ref ConsoleKeyInfo [] cki
- )
- {
- ConsoleKey ck = 0;
- ConsoleModifiers mod = 0;
-
- while (code == 0)
- {
- code = Curses.get_wch (out wch2);
- var consoleKeyInfo = new ConsoleKeyInfo ((char)wch2, 0, false, false, false);
-
- if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
- {
- EscSeqUtils.DecodeEscSeq (
- null,
- ref consoleKeyInfo,
- ref ck,
- cki,
- ref mod,
- out _,
- out _,
- out _,
- out _,
- out bool isKeyMouse,
- out List mouseFlags,
- out Point pos,
- out _,
- ProcessMouseEvent
- );
-
- if (isKeyMouse)
- {
- foreach (MouseFlags mf in mouseFlags)
- {
- ProcessMouseEvent (mf, pos);
- }
-
- cki = null;
-
- if (wch2 == 27)
- {
- cki = EscSeqUtils.ResizeArray (
- new ConsoleKeyInfo (
- (char)KeyCode.Esc,
- 0,
- false,
- false,
- false
- ),
- cki
- );
- }
- }
- else
- {
- k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
- keyEventArgs = new Key (k);
- OnKeyDown (keyEventArgs);
- }
- }
- else
- {
- cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki);
- }
- }
- }
-
- private static KeyCode MapCursesKey (int cursesKey)
- {
- switch (cursesKey)
- {
- case Curses.KeyF1: return KeyCode.F1;
- case Curses.KeyF2: return KeyCode.F2;
- case Curses.KeyF3: return KeyCode.F3;
- case Curses.KeyF4: return KeyCode.F4;
- case Curses.KeyF5: return KeyCode.F5;
- case Curses.KeyF6: return KeyCode.F6;
- case Curses.KeyF7: return KeyCode.F7;
- case Curses.KeyF8: return KeyCode.F8;
- case Curses.KeyF9: return KeyCode.F9;
- case Curses.KeyF10: return KeyCode.F10;
- case Curses.KeyF11: return KeyCode.F11;
- case Curses.KeyF12: return KeyCode.F12;
- case Curses.KeyUp: return KeyCode.CursorUp;
- case Curses.KeyDown: return KeyCode.CursorDown;
- case Curses.KeyLeft: return KeyCode.CursorLeft;
- case Curses.KeyRight: return KeyCode.CursorRight;
- case Curses.KeyHome: return KeyCode.Home;
- case Curses.KeyEnd: return KeyCode.End;
- case Curses.KeyNPage: return KeyCode.PageDown;
- case Curses.KeyPPage: return KeyCode.PageUp;
- case Curses.KeyDeleteChar: return KeyCode.Delete;
- case Curses.KeyInsertChar: return KeyCode.Insert;
- case Curses.KeyTab: return KeyCode.Tab;
- case Curses.KeyBackTab: return KeyCode.Tab | KeyCode.ShiftMask;
- case Curses.KeyBackspace: return KeyCode.Backspace;
- case Curses.ShiftKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask;
- case Curses.ShiftKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask;
- case Curses.ShiftKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask;
- case Curses.ShiftKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask;
- case Curses.ShiftKeyHome: return KeyCode.Home | KeyCode.ShiftMask;
- case Curses.ShiftKeyEnd: return KeyCode.End | KeyCode.ShiftMask;
- case Curses.ShiftKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask;
- case Curses.ShiftKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask;
- case Curses.AltKeyUp: return KeyCode.CursorUp | KeyCode.AltMask;
- case Curses.AltKeyDown: return KeyCode.CursorDown | KeyCode.AltMask;
- case Curses.AltKeyLeft: return KeyCode.CursorLeft | KeyCode.AltMask;
- case Curses.AltKeyRight: return KeyCode.CursorRight | KeyCode.AltMask;
- case Curses.AltKeyHome: return KeyCode.Home | KeyCode.AltMask;
- case Curses.AltKeyEnd: return KeyCode.End | KeyCode.AltMask;
- case Curses.AltKeyNPage: return KeyCode.PageDown | KeyCode.AltMask;
- case Curses.AltKeyPPage: return KeyCode.PageUp | KeyCode.AltMask;
- case Curses.CtrlKeyUp: return KeyCode.CursorUp | KeyCode.CtrlMask;
- case Curses.CtrlKeyDown: return KeyCode.CursorDown | KeyCode.CtrlMask;
- case Curses.CtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.CtrlMask;
- case Curses.CtrlKeyRight: return KeyCode.CursorRight | KeyCode.CtrlMask;
- case Curses.CtrlKeyHome: return KeyCode.Home | KeyCode.CtrlMask;
- case Curses.CtrlKeyEnd: return KeyCode.End | KeyCode.CtrlMask;
- case Curses.CtrlKeyNPage: return KeyCode.PageDown | KeyCode.CtrlMask;
- case Curses.CtrlKeyPPage: return KeyCode.PageUp | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftCtrlKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.CtrlMask;
- case Curses.ShiftAltKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.ShiftAltKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.AltMask;
- case Curses.AltCtrlKeyNPage: return KeyCode.PageDown | KeyCode.AltMask | KeyCode.CtrlMask;
- case Curses.AltCtrlKeyPPage: return KeyCode.PageUp | KeyCode.AltMask | KeyCode.CtrlMask;
- case Curses.AltCtrlKeyHome: return KeyCode.Home | KeyCode.AltMask | KeyCode.CtrlMask;
- case Curses.AltCtrlKeyEnd: return KeyCode.End | KeyCode.AltMask | KeyCode.CtrlMask;
- default: return KeyCode.Null;
- }
- }
-
- private void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
+ internal override void End ()
{
- bool WasButtonReleased (MouseFlags flag)
- {
- return flag.HasFlag (MouseFlags.Button1Released)
- || flag.HasFlag (MouseFlags.Button2Released)
- || flag.HasFlag (MouseFlags.Button3Released)
- || flag.HasFlag (MouseFlags.Button4Released);
- }
-
- bool IsButtonNotPressed (MouseFlags flag)
- {
- return !flag.HasFlag (MouseFlags.Button1Pressed)
- && !flag.HasFlag (MouseFlags.Button2Pressed)
- && !flag.HasFlag (MouseFlags.Button3Pressed)
- && !flag.HasFlag (MouseFlags.Button4Pressed);
- }
-
- bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
- {
- return flag.HasFlag (MouseFlags.Button1Clicked)
- || flag.HasFlag (MouseFlags.Button2Clicked)
- || flag.HasFlag (MouseFlags.Button3Clicked)
- || flag.HasFlag (MouseFlags.Button4Clicked)
- || flag.HasFlag (MouseFlags.Button1DoubleClicked)
- || flag.HasFlag (MouseFlags.Button2DoubleClicked)
- || flag.HasFlag (MouseFlags.Button3DoubleClicked)
- || flag.HasFlag (MouseFlags.Button4DoubleClicked);
- }
-
- Debug.WriteLine ($"CursesDriver: ({pos.X},{pos.Y}) - {mouseFlag}");
+ StopReportingMouseMoves ();
+ SetCursorVisibility (CursorVisibility.Default);
- if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
+ if (RunningUnitTests)
{
return;
}
- _lastMouseFlags = mouseFlag;
-
- var me = new MouseEventArgs { Flags = mouseFlag, Position = pos };
- //Debug.WriteLine ($"CursesDriver: ({me.Position}) - {me.Flags}");
-
- OnMouseEvent (me);
- }
-
- #region Color Handling
-
- /// Creates an Attribute from the provided curses-based foreground and background color numbers
- /// Contains the curses color number for the foreground (color, plus any attributes)
- /// Contains the curses color number for the background (color, plus any attributes)
- ///
- private static Attribute MakeColor (short foreground, short background)
- {
- //var v = (short)((ushort)foreground | (background << 4));
- var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff));
-
- // TODO: for TrueColor - Use InitExtendedPair
- Curses.InitColorPair (v, foreground, background);
+ // throws away any typeahead that has been typed by
+ // the user and has not yet been read by the program.
+ Curses.flushinp ();
- return new Attribute (
- Curses.ColorPair (v),
- CursesColorNumberToColorName16 (foreground),
- CursesColorNumberToColorName16 (background)
- );
+ Curses.endwin ();
}
- ///
- ///
- /// In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4
- /// bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are
- /// converted to curses color encoding before being encoded.
- ///
- public override Attribute MakeColor (in Color foreground, in Color background)
- {
- if (!RunningUnitTests && Force16Colors)
- {
- return MakeColor (
- ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()),
- ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ())
- );
- }
-
- return new Attribute (
- 0,
- foreground,
- background
- );
- }
+ #endregion Init/End/MainLoop
- private static short ColorNameToCursesColorNumber (ColorName16 color)
+ public static bool Is_WSL_Platform ()
{
- switch (color)
- {
- case ColorName16.Black:
- return Curses.COLOR_BLACK;
- case ColorName16.Blue:
- return Curses.COLOR_BLUE;
- case ColorName16.Green:
- return Curses.COLOR_GREEN;
- case ColorName16.Cyan:
- return Curses.COLOR_CYAN;
- case ColorName16.Red:
- return Curses.COLOR_RED;
- case ColorName16.Magenta:
- return Curses.COLOR_MAGENTA;
- case ColorName16.Yellow:
- return Curses.COLOR_YELLOW;
- case ColorName16.Gray:
- return Curses.COLOR_WHITE;
- case ColorName16.DarkGray:
- return Curses.COLOR_GRAY;
- case ColorName16.BrightBlue:
- return Curses.COLOR_BLUE | Curses.COLOR_GRAY;
- case ColorName16.BrightGreen:
- return Curses.COLOR_GREEN | Curses.COLOR_GRAY;
- case ColorName16.BrightCyan:
- return Curses.COLOR_CYAN | Curses.COLOR_GRAY;
- case ColorName16.BrightRed:
- return Curses.COLOR_RED | Curses.COLOR_GRAY;
- case ColorName16.BrightMagenta:
- return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY;
- case ColorName16.BrightYellow:
- return Curses.COLOR_YELLOW | Curses.COLOR_GRAY;
- case ColorName16.White:
- return Curses.COLOR_WHITE | Curses.COLOR_GRAY;
- }
-
- throw new ArgumentException ("Invalid color code");
- }
+ // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell
+ //if (new CursesClipboard ().IsSupported) {
+ // // If xclip is installed on Linux under WSL, this will return true.
+ // return false;
+ //}
+ (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true);
- private static ColorName16 CursesColorNumberToColorName16 (short color)
- {
- switch (color)
+ if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL"))
{
- case Curses.COLOR_BLACK:
- return ColorName16.Black;
- case Curses.COLOR_BLUE:
- return ColorName16.Blue;
- case Curses.COLOR_GREEN:
- return ColorName16.Green;
- case Curses.COLOR_CYAN:
- return ColorName16.Cyan;
- case Curses.COLOR_RED:
- return ColorName16.Red;
- case Curses.COLOR_MAGENTA:
- return ColorName16.Magenta;
- case Curses.COLOR_YELLOW:
- return ColorName16.Yellow;
- case Curses.COLOR_WHITE:
- return ColorName16.Gray;
- case Curses.COLOR_GRAY:
- return ColorName16.DarkGray;
- case Curses.COLOR_BLUE | Curses.COLOR_GRAY:
- return ColorName16.BrightBlue;
- case Curses.COLOR_GREEN | Curses.COLOR_GRAY:
- return ColorName16.BrightGreen;
- case Curses.COLOR_CYAN | Curses.COLOR_GRAY:
- return ColorName16.BrightCyan;
- case Curses.COLOR_RED | Curses.COLOR_GRAY:
- return ColorName16.BrightRed;
- case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY:
- return ColorName16.BrightMagenta;
- case Curses.COLOR_YELLOW | Curses.COLOR_GRAY:
- return ColorName16.BrightYellow;
- case Curses.COLOR_WHITE | Curses.COLOR_GRAY:
- return ColorName16.White;
+ return true;
}
- throw new ArgumentException ("Invalid curses color code");
+ return false;
}
- #endregion
+ ///
+ internal override void WriteRaw (string ansi) { _mainLoopDriver?.WriteRaw (ansi); }
}
+// TODO: One type per file - move to another file
internal static class Platform
{
private static int _suspendSignal;
@@ -1224,4 +845,4 @@ private static int GetSuspendSignal ()
[DllImport ("libc")]
private static extern int uname (nint buf);
-}
+}
\ No newline at end of file
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c
new file mode 100644
index 0000000000..6ca9471d2c
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c
@@ -0,0 +1,12 @@
+#include
+#include
+
+// Used to get the value of the TIOCGWINSZ variable,
+// which may have different values on different Unix operating systems.
+// Linux=0x005413
+// Darwin and OpenBSD=0x40087468,
+// Solaris=0x005468
+// See https://stackoverflow.com/questions/16237137/what-is-termios-tiocgwinsz
+int get_tiocgwinsz_value() {
+ return TIOCGWINSZ;
+}
\ No newline at end of file
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.sh b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.sh
new file mode 100644
index 0000000000..e94706eb18
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Create output directory if it doesn't exist
+mkdir -p ../../compiled-binaries
+
+# Determine the output file extension based on the OS
+if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+ OUTPUT_FILE="../../compiled-binaries/libGetTIOCGWINSZ.so"
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+ OUTPUT_FILE="../../compiled-binaries/libGetTIOCGWINSZ.dylib"
+else
+ echo "Unsupported OS: $OSTYPE"
+ exit 1
+fi
+
+# Compile the C file
+gcc -shared -fPIC -o "$OUTPUT_FILE" GetTIOCGWINSZ.c
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
index 34139815c5..aa9579f4db 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
@@ -1,7 +1,9 @@
+#nullable enable
//
// mainloop.cs: Linux/Curses MainLoop implementation.
//
+using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace Terminal.Gui;
@@ -11,7 +13,7 @@ namespace Terminal.Gui;
/// In addition to the general functions of the MainLoop, the Unix version can watch file descriptors using the
/// AddWatch methods.
///
-internal class UnixMainLoop : IMainLoopDriver
+internal class UnixMainLoop (ConsoleDriver consoleDriver) : IMainLoopDriver
{
/// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions.
[Flags]
@@ -36,30 +38,17 @@ public enum Condition : short
PollNval = 32
}
- public const int KEY_RESIZE = unchecked ((int)0xffffffffffffffff);
- private static readonly nint _ignore = Marshal.AllocHGlobal (1);
+ private readonly CursesDriver _cursesDriver = (CursesDriver)consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+ private MainLoop? _mainLoop;
+ private Pollfd []? _pollMap;
+ private readonly ConcurrentQueue _pollDataQueue = new ();
+ private readonly ManualResetEventSlim _eventReady = new (false);
+ ManualResetEventSlim IMainLoopDriver.WaitForInput { get; set; } = new (false);
+ private readonly ManualResetEventSlim _windowSizeChange = new (false);
+ private readonly CancellationTokenSource _eventReadyTokenSource = new ();
+ private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
- private readonly CursesDriver _cursesDriver;
- private readonly Dictionary _descriptorWatchers = new ();
- private readonly int [] _wakeUpPipes = new int [2];
- private MainLoop _mainLoop;
- private bool _pollDirty = true;
- private Pollfd [] _pollMap;
- private bool _winChanged;
-
- public UnixMainLoop (ConsoleDriver consoleDriver = null)
- {
- // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does.
- _cursesDriver = (CursesDriver)Application.Driver;
- }
-
- void IMainLoopDriver.Wakeup ()
- {
- if (!ConsoleDriver.RunningUnitTests)
- {
- write (_wakeUpPipes [1], _ignore, 1);
- }
- }
+ void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
void IMainLoopDriver.Setup (MainLoop mainLoop)
{
@@ -72,151 +61,380 @@ void IMainLoopDriver.Setup (MainLoop mainLoop)
try
{
- pipe (_wakeUpPipes);
-
- AddWatch (
- _wakeUpPipes [0],
- Condition.PollIn,
- ml =>
- {
- read (_wakeUpPipes [0], _ignore, 1);
-
- return true;
- }
- );
+ // Setup poll for stdin (fd 0)
+ _pollMap = new Pollfd [1];
+ _pollMap [0].fd = 0; // stdin (file descriptor 0)
+ _pollMap [0].events = (short)Condition.PollIn; // Monitor input for reading
}
catch (DllNotFoundException e)
{
throw new NotSupportedException ("libc not found", e);
}
+
+ AnsiEscapeSequenceRequestUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed;
+
+ Task.Run (CursesInputHandler, _inputHandlerTokenSource.Token);
+ Task.Run (WindowSizeHandler, _inputHandlerTokenSource.Token);
}
- bool IMainLoopDriver.EventsPending ()
- {
- UpdatePollMap ();
+ private static readonly int TIOCGWINSZ = GetTIOCGWINSZValue ();
+
+ private const string PlaceholderLibrary = "compiled-binaries/libGetTIOCGWINSZ"; // Placeholder, won't directly load
+
+ [DllImport (PlaceholderLibrary, EntryPoint = "get_tiocgwinsz_value")]
+ private static extern int GetTIOCGWINSZValueInternal ();
- bool checkTimersResult = _mainLoop.CheckTimersAndIdleHandlers (out int pollTimeout);
+ public static int GetTIOCGWINSZValue ()
+ {
+ // Determine the correct library path based on the OS
+ string libraryPath = Path.Combine (
+ AppContext.BaseDirectory,
+ "compiled-binaries",
+ RuntimeInformation.IsOSPlatform (OSPlatform.OSX) ? "libGetTIOCGWINSZ.dylib" : "libGetTIOCGWINSZ.so");
- int n = poll (_pollMap, (uint)_pollMap.Length, pollTimeout);
+ // Load the native library manually
+ nint handle = NativeLibrary.Load (libraryPath);
- if (n == KEY_RESIZE)
+ // Ensure the handle is valid
+ if (handle == nint.Zero)
{
- _winChanged = true;
+ throw new DllNotFoundException ($"Unable to load library: {libraryPath}");
}
- return checkTimersResult || n >= KEY_RESIZE;
+ return GetTIOCGWINSZValueInternal ();
}
- void IMainLoopDriver.Iteration ()
+ private void EscSeqUtils_ContinuousButtonPressed (object? sender, MouseEventArgs e)
{
- if (_winChanged)
- {
- _winChanged = false;
- _cursesDriver.ProcessInput ();
+ _pollDataQueue.Enqueue (EnqueueMouseEvent (e.Flags, e.Position));
+ }
- // This is needed on the mac. See https://github.com/gui-cs/Terminal.Gui/pull/2922#discussion_r1365992426
- _cursesDriver.ProcessWinChange ();
- }
+ private void WindowSizeHandler ()
+ {
+ var ws = new Winsize ();
+ ioctl (0, TIOCGWINSZ, ref ws);
- if (_pollMap is null)
- {
- return;
- }
+ // Store initial window size
+ int rows = ws.ws_row;
+ int cols = ws.ws_col;
- foreach (Pollfd p in _pollMap)
+ while (_inputHandlerTokenSource is { IsCancellationRequested: false })
{
- Watch watch;
+ try
+ {
+ _windowSizeChange.Wait (_inputHandlerTokenSource.Token);
+ _windowSizeChange.Reset ();
+
+ while (!_inputHandlerTokenSource.IsCancellationRequested)
+ {
+ // Wait for a while then check if screen has changed sizes
+ Task.Delay (500, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
+
+ ioctl (0, TIOCGWINSZ, ref ws);
- if (p.revents == 0)
+ if (rows != ws.ws_row || cols != ws.ws_col)
+ {
+ rows = ws.ws_row;
+ cols = ws.ws_col;
+
+ _pollDataQueue.Enqueue (EnqueueWindowSizeEvent (rows, cols));
+
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
{
- continue;
+ return;
}
- if (!_descriptorWatchers.TryGetValue (p.fd, out watch))
+ _eventReady.Set ();
+ }
+ }
+
+ bool IMainLoopDriver.ForceRead { get; set; }
+ private int _retries;
+
+ private void CursesInputHandler ()
+ {
+ while (_mainLoop is { })
+ {
+ try
{
- continue;
+ if (!_inputHandlerTokenSource.IsCancellationRequested && !((IMainLoopDriver)this).ForceRead)
+ {
+ try
+ {
+ ((IMainLoopDriver)this).WaitForInput.Wait (_inputHandlerTokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ if (ex is OperationCanceledException or ObjectDisposedException)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ ((IMainLoopDriver)this).WaitForInput.Reset ();
+ }
+
+ ProcessInputQueue ();
}
-
- if (!watch.Callback (_mainLoop))
+ catch (OperationCanceledException)
{
- _descriptorWatchers.Remove (p.fd);
+ return;
}
}
}
- void IMainLoopDriver.TearDown ()
+ private void ProcessInputQueue ()
{
- _descriptorWatchers?.Clear ();
+ if (_pollDataQueue.Count == 0 || ((IMainLoopDriver)this).ForceRead)
+ {
+ while (!_inputHandlerTokenSource.IsCancellationRequested)
+ {
+ try
+ {
+ Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ int n = poll (_pollMap!, (uint)_pollMap!.Length, 0);
+
+ if (n > 0)
+ {
+ // Check if stdin has data
+ if ((_pollMap [0].revents & (int)Condition.PollIn) != 0)
+ {
+ // Allocate memory for the buffer
+ var buf = new byte [2048];
+ nint bufPtr = Marshal.AllocHGlobal (buf.Length);
+
+ try
+ {
+ // Read from the stdin
+ int bytesRead = read (_pollMap [0].fd, bufPtr, buf.Length);
+
+ if (bytesRead > 0)
+ {
+ // Copy the data from unmanaged memory to a byte array
+ var buffer = new byte [bytesRead];
+ Marshal.Copy (bufPtr, buffer, 0, bytesRead);
+
+ // Convert the byte array to a string (assuming UTF-8 encoding)
+ string data = Encoding.UTF8.GetString (buffer);
+
+ if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is { })
+ {
+ data = data.Insert (0, AnsiEscapeSequenceRequestUtils.ToString (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos));
+ AnsiEscapeSequenceRequestUtils.IncompleteCkInfos = null;
+ }
+
+ // Enqueue the data
+ ProcessEnqueuePollData (data);
+ }
+ }
+ finally
+ {
+ // Free the allocated memory
+ Marshal.FreeHGlobal (bufPtr);
+ }
+ }
+
+ if (_retries > 0)
+ {
+ _retries = 0;
+ }
+
+ break;
+ }
+
+ if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && AnsiEscapeSequenceRequests.Statuses.Count > 0)
+ {
+ if (_retries > 1)
+ {
+ if (AnsiEscapeSequenceRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? seqReqStatus))
+ {
+ lock (seqReqStatus.AnsiRequest._responseLock)
+ {
+ AnsiEscapeSequenceRequests.Statuses.TryDequeue (out _);
+
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (null, seqReqStatus.AnsiRequest);
+ }
+ }
+
+ _retries = 0;
+ }
+ else
+ {
+ _retries++;
+ }
+ }
+ else
+ {
+ _retries = 0;
+ }
+ }
+ }
- _mainLoop = null;
+ if (_pollDataQueue.Count > 0)
+ {
+ _eventReady.Set ();
+ }
}
- /// Watches a file descriptor for activity.
- ///
- /// When the condition is met, the provided callback is invoked. If the callback returns false, the watch is
- /// automatically removed. The return value is a token that represents this watch, you can use this token to remove the
- /// watch by calling RemoveWatch.
- ///
- internal object AddWatch (int fileDescriptor, Condition condition, Func callback)
+ private void ProcessEnqueuePollData (string pollData)
{
- if (callback is null)
+ foreach (string split in AnsiEscapeSequenceRequestUtils.SplitEscapeRawString (pollData))
{
- throw new ArgumentNullException (nameof (callback));
+ EnqueuePollData (split);
}
-
- var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor };
- _descriptorWatchers [fileDescriptor] = watch;
- _pollDirty = true;
-
- return watch;
}
- /// Removes an active watch from the mainloop.
- /// The token parameter is the value returned from AddWatch
- internal void RemoveWatch (object token)
+ private void EnqueuePollData (string pollDataPart)
{
- if (!ConsoleDriver.RunningUnitTests)
+ ConsoleKeyInfo [] cki = AnsiEscapeSequenceRequestUtils.ToConsoleKeyInfoArray (pollDataPart);
+
+ ConsoleKey key = 0;
+ ConsoleModifiers mod = 0;
+ ConsoleKeyInfo newConsoleKeyInfo = default;
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ 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,
+ out Point pos,
+ out AnsiEscapeSequenceRequestStatus? seqReqStatus,
+ AnsiEscapeSequenceRequestUtils.ProcessMouseEvent
+ );
+
+ if (isMouse)
{
- if (token is not Watch watch)
+ foreach (MouseFlags mf in mouseFlags)
{
- return;
+ _pollDataQueue.Enqueue (EnqueueMouseEvent (mf, pos));
}
- _descriptorWatchers.Remove (watch.File);
+ return;
+ }
+
+ if (newConsoleKeyInfo != default)
+ {
+ _pollDataQueue.Enqueue (EnqueueKeyboardEvent (newConsoleKeyInfo));
}
}
- [DllImport ("libc")]
- private static extern int pipe ([In] [Out] int [] pipes);
+ private PollData EnqueueMouseEvent (MouseFlags mouseFlags, Point pos)
+ {
+ var mouseEvent = new MouseEvent { Position = pos, MouseFlags = mouseFlags };
- [DllImport ("libc")]
- private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout);
+ return new () { EventType = EventType.Mouse, MouseEvent = mouseEvent };
+ }
- [DllImport ("libc")]
- private static extern int read (int fd, nint buf, nint n);
+ private PollData EnqueueKeyboardEvent (ConsoleKeyInfo keyInfo)
+ {
+ return new () { EventType = EventType.Key, KeyEvent = keyInfo };
+ }
- private void UpdatePollMap ()
+ private PollData EnqueueWindowSizeEvent (int rows, int cols)
{
- if (!_pollDirty)
+ return new () { EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (cols, rows) } };
+ }
+
+ bool IMainLoopDriver.EventsPending ()
+ {
+ ((IMainLoopDriver)this).WaitForInput.Set ();
+ _windowSizeChange.Set ();
+
+ if (_mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout))
{
- return;
+ return true;
+ }
+
+ try
+ {
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+ _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return true;
+ }
+ finally
+ {
+ _eventReady.Reset ();
}
- _pollDirty = false;
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+ return _pollDataQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+ }
- _pollMap = new Pollfd [_descriptorWatchers.Count];
- var i = 0;
+ return true;
+ }
- foreach (int fd in _descriptorWatchers.Keys)
+ void IMainLoopDriver.Iteration ()
+ {
+ // Dequeue and process the data
+ while (_pollDataQueue.TryDequeue (out PollData inputRecords))
{
- _pollMap [i].fd = fd;
- _pollMap [i].events = (short)_descriptorWatchers [fd].Condition;
- i++;
+ _cursesDriver.ProcessInput (inputRecords);
}
}
+ void IMainLoopDriver.TearDown ()
+ {
+ AnsiEscapeSequenceRequestUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed;
+
+ _inputHandlerTokenSource.Cancel ();
+ _inputHandlerTokenSource.Dispose ();
+ ((IMainLoopDriver)this).WaitForInput?.Dispose ();
+
+ _windowSizeChange.Dispose();
+
+ _pollDataQueue.Clear ();
+
+ _eventReadyTokenSource.Cancel ();
+ _eventReadyTokenSource.Dispose ();
+ _eventReady.Dispose ();
+
+ _mainLoop = null;
+ }
+
+ internal void WriteRaw (string ansiRequest)
+ {
+ // Write to stdout (fd 1)
+ write (STDOUT_FILENO, ansiRequest, ansiRequest.Length);
+ }
+
+ [DllImport ("libc")]
+ private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout);
+
+ [DllImport ("libc")]
+ private static extern int read (int fd, nint buf, nint n);
+
+ // File descriptor for stdout
+ private const int STDOUT_FILENO = 1;
+
[DllImport ("libc")]
- private static extern int write (int fd, nint buf, nint n);
+ private static extern int write (int fd, string buf, int n);
+
+ [DllImport ("libc", SetLastError = true)]
+ private static extern int ioctl (int fd, int request, ref Winsize ws);
[StructLayout (LayoutKind.Sequential)]
private struct Pollfd
@@ -226,10 +444,74 @@ private struct Pollfd
public readonly short revents;
}
- private class Watch
+ ///
+ /// Window or terminal size structure. This information is stored by the kernel in order to provide a consistent
+ /// interface, but is not used by the kernel.
+ ///
+ [StructLayout (LayoutKind.Sequential)]
+ public struct Winsize
+ {
+ public ushort ws_row; // Number of rows
+ public ushort ws_col; // Number of columns
+ public ushort ws_xpixel; // Width in pixels (unused)
+ public ushort ws_ypixel; // Height in pixels (unused)
+ }
+
+ #region Events
+
+ public enum EventType
{
- public Func Callback;
- public Condition Condition;
- public int File;
+ Key = 1,
+ Mouse = 2,
+ WindowSize = 3
}
+
+ public struct MouseEvent
+ {
+ public Point Position;
+ public MouseFlags MouseFlags;
+ }
+
+ public struct WindowSizeEvent
+ {
+ public Size Size;
+ }
+
+ public struct PollData
+ {
+ public EventType EventType;
+ public ConsoleKeyInfo KeyEvent;
+ public MouseEvent MouseEvent;
+ public WindowSizeEvent WindowSizeEvent;
+
+ public readonly override string ToString ()
+ {
+ return (EventType switch
+ {
+ EventType.Key => ToString (KeyEvent),
+ EventType.Mouse => MouseEvent.ToString (),
+ EventType.WindowSize => WindowSizeEvent.ToString (),
+ _ => "Unknown event type: " + EventType
+ })!;
+ }
+
+ /// Prints a ConsoleKeyInfoEx structure
+ ///
+ ///
+ 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})]";
+ }
+ }
+
+ #endregion
}
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
index 16caaa05ca..d791194019 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs
@@ -143,6 +143,19 @@ public static bool CheckWinChange ()
return false;
}
+ public static bool ChangeWindowSize (int l, int c)
+ {
+ if (l != lines || c != cols)
+ {
+ lines = l;
+ cols = c;
+
+ return true;
+ }
+
+ return false;
+ }
+
public static int clearok (nint win, bool bf) { return methods.clearok (win, bf); }
public static int COLOR_PAIRS () { return methods.COLOR_PAIRS (); }
public static int curs_set (int visibility) { return methods.curs_set (visibility); }
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
index 5700b779f8..2984147a21 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs
@@ -55,8 +55,6 @@ public partial class Curses
public const int COLOR_GRAY = 0x8;
public const int KEY_CODE_YES = 0x100;
public const int ERR = unchecked ((int)0xffffffff);
- public const int TIOCGWINSZ = 0x5413;
- public const int TIOCGWINSZ_MAC = 0x40087468;
[Flags]
public enum Event : long
{
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
deleted file mode 100644
index 29ef5afa79..0000000000
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-namespace Terminal.Gui;
-
-///
-/// Represents the status of an ANSI escape sequence request made to the terminal using
-/// .
-///
-///
-public class EscSeqReqStatus
-{
- /// Creates a new state of escape sequence request.
- /// The terminator.
- /// The number of requests.
- public EscSeqReqStatus (string terminator, int numReq)
- {
- Terminator = terminator;
- NumRequests = NumOutstanding = numReq;
- }
-
- /// Gets the number of unfinished requests.
- public int NumOutstanding { get; set; }
-
- /// Gets the number of requests.
- public int NumRequests { get; }
-
- /// Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).
- public string Terminator { get; }
-}
-
-// TODO: This class is a singleton. It should use the singleton pattern.
-///
-/// Manages ANSI Escape Sequence requests and responses. The list of contains the
-/// status of the request. Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator).
-///
-public class EscSeqRequests
-{
- /// Gets the list.
- public List Statuses { get; } = new ();
-
- ///
- /// Adds a new request for the ANSI Escape Sequence defined by . Adds a
- /// instance to list.
- ///
- /// The terminator.
- /// The number of requests.
- public void Add (string terminator, int numReq = 1)
- {
- lock (Statuses)
- {
- EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
-
- if (found is null)
- {
- Statuses.Add (new EscSeqReqStatus (terminator, numReq));
- }
- else if (found is { } && found.NumOutstanding < found.NumRequests)
- {
- found.NumOutstanding = Math.Min (found.NumOutstanding + numReq, found.NumRequests);
- }
- }
- }
-
- ///
- /// Indicates if a with the exists in the
- /// list.
- ///
- ///
- /// if exist, otherwise.
- public bool HasResponse (string terminator)
- {
- lock (Statuses)
- {
- EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
-
- if (found is null)
- {
- return false;
- }
-
- if (found is { NumOutstanding: > 0 })
- {
- return true;
- }
-
- // BUGBUG: Why does an API that returns a bool remove the entry from the list?
- // NetDriver and Unit tests never exercise this line of code. Maybe Curses does?
- Statuses.Remove (found);
-
- return false;
- }
- }
-
- ///
- /// Removes a request defined by . If a matching is
- /// found and the number of outstanding requests is greater than 0, the number of outstanding requests is decremented.
- /// If the number of outstanding requests is 0, the is removed from
- /// .
- ///
- /// The terminating string.
- public void Remove (string terminator)
- {
- lock (Statuses)
- {
- EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator);
-
- if (found is null)
- {
- return;
- }
-
- if (found is { } && found.NumOutstanding == 0)
- {
- Statuses.Remove (found);
- }
- else if (found is { } && found.NumOutstanding > 0)
- {
- found.NumOutstanding--;
-
- if (found.NumOutstanding == 0)
- {
- Statuses.Remove (found);
- }
- }
- }
- }
-}
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
index a5f297ee69..7d82737ccb 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
@@ -392,6 +392,9 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al
MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
}
+ ///
+ internal override void WriteRaw (string ansi) { throw new NotImplementedException (); }
+
public void SetBufferSize (int width, int height)
{
FakeConsole.SetBufferSize (width, height);
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
index d90caace7b..e6ea9cab02 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs
@@ -3,6 +3,8 @@
internal class FakeMainLoop : IMainLoopDriver
{
public Action MockKeyPressed;
+ public bool ForceRead { get; set; }
+ public ManualResetEventSlim WaitForInput { get; set; } = new ();
public FakeMainLoop (ConsoleDriver consoleDriver = null)
{
diff --git a/Terminal.Gui/ConsoleDrivers/KeyCode.cs b/Terminal.Gui/ConsoleDrivers/KeyCode.cs
new file mode 100644
index 0000000000..183322ec80
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/KeyCode.cs
@@ -0,0 +1,321 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// The enumeration encodes key information from s and provides a
+/// consistent way for application code to specify keys and receive key events.
+///
+/// The class provides a higher-level abstraction, with helper methods and properties for
+/// common operations. For example, and provide a convenient way
+/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed.
+///
+///
+///
+///
+/// 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. , , etc.).
+/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent
+/// *lowercase*, un-shifted characters.
+///
+///
+/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. ,
+/// , etc.).
+///
+///
+/// The shift modifiers (, , and
+/// ) can be combined (with logical or) with the other key codes to represent shifted
+/// keys. For example, the enum value represents the un-shifted 'a' key, while
+/// | represents the 'A' key (shifted 'a' key). Likewise,
+/// | represents the 'Alt+A' key combination.
+///
+///
+/// All other keys that produce a printable character are encoded as the Unicode value of the character. For
+/// example, the for the '!' character is 33, which is the Unicode value for '!'. Likewise,
+/// `â` is 226, `Â` is 194, etc.
+///
+///
+/// If the is set, then the value is that of the special mask, otherwise, the value is
+/// the one of the lower bits (as extracted by ).
+///
+///
+[Flags]
+public enum KeyCode : uint
+{
+ ///
+ /// 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.
+ ///
+ CharMask = 0x_f_ffff,
+
+ ///
+ /// If the is set, then the value is that of the special mask, otherwise, the value is
+ /// in the lower bits (as extracted by ).
+ ///
+ SpecialMask = 0x_fff0_0000,
+
+ ///
+ /// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by
+ /// removing the ShiftMask.
+ ///
+ ShiftMask = 0x_1000_0000,
+
+ ///
+ /// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by
+ /// removing the AltMask.
+ ///
+ AltMask = 0x_8000_0000,
+
+ ///
+ /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by
+ /// removing the CtrlMask.
+ ///
+ CtrlMask = 0x_4000_0000,
+
+ /// The key code representing an invalid or empty key.
+ Null = 0,
+
+ /// Backspace key.
+ Backspace = 8,
+
+ /// The key code for the tab key (forwards tab key).
+ Tab = 9,
+
+ /// The key code for the return key.
+ Enter = ConsoleKey.Enter,
+
+ /// The key code for the clear key.
+ Clear = 12,
+
+ /// The key code for the escape key.
+ Esc = 27,
+
+ /// The key code for the space bar key.
+ Space = 32,
+
+ /// Digit 0.
+ D0 = 48,
+
+ /// Digit 1.
+ D1,
+
+ /// Digit 2.
+ D2,
+
+ /// Digit 3.
+ D3,
+
+ /// Digit 4.
+ D4,
+
+ /// Digit 5.
+ D5,
+
+ /// Digit 6.
+ D6,
+
+ /// Digit 7.
+ D7,
+
+ /// Digit 8.
+ D8,
+
+ /// Digit 9.
+ D9,
+
+ /// The key code for the A key
+ A = 65,
+
+ /// The key code for the B key
+ B,
+
+ /// The key code for the C key
+ C,
+
+ /// The key code for the D key
+ D,
+
+ /// The key code for the E key
+ E,
+
+ /// The key code for the F key
+ F,
+
+ /// The key code for the G key
+ G,
+
+ /// The key code for the H key
+ H,
+
+ /// The key code for the I key
+ I,
+
+ /// The key code for the J key
+ J,
+
+ /// The key code for the K key
+ K,
+
+ /// The key code for the L key
+ L,
+
+ /// The key code for the M key
+ M,
+
+ /// The key code for the N key
+ N,
+
+ /// The key code for the O key
+ O,
+
+ /// The key code for the P key
+ P,
+
+ /// The key code for the Q key
+ Q,
+
+ /// The key code for the R key
+ R,
+
+ /// The key code for the S key
+ S,
+
+ /// The key code for the T key
+ T,
+
+ /// The key code for the U key
+ U,
+
+ /// The key code for the V key
+ V,
+
+ /// The key code for the W key
+ W,
+
+ /// The key code for the X key
+ X,
+
+ /// The key code for the Y key
+ Y,
+
+ /// The key code for the Z key
+ Z,
+
+ /////
+ ///// The key code for the Delete key.
+ /////
+ //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.
+
+ /// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys.
+ MaxCodePoint = 0x10FFFF,
+
+ /// Cursor up key
+ CursorUp = MaxCodePoint + ConsoleKey.UpArrow,
+
+ /// Cursor down key.
+ CursorDown = MaxCodePoint + ConsoleKey.DownArrow,
+
+ /// Cursor left key.
+ CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow,
+
+ /// Cursor right key.
+ CursorRight = MaxCodePoint + ConsoleKey.RightArrow,
+
+ /// Page Up key.
+ PageUp = MaxCodePoint + ConsoleKey.PageUp,
+
+ /// Page Down key.
+ PageDown = MaxCodePoint + ConsoleKey.PageDown,
+
+ /// Home key.
+ Home = MaxCodePoint + ConsoleKey.Home,
+
+ /// End key.
+ End = MaxCodePoint + ConsoleKey.End,
+
+ /// Insert (INS) key.
+ Insert = MaxCodePoint + ConsoleKey.Insert,
+
+ /// Delete (DEL) key.
+ Delete = MaxCodePoint + ConsoleKey.Delete,
+
+ /// Print screen character key.
+ PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen,
+
+ /// F1 key.
+ F1 = MaxCodePoint + ConsoleKey.F1,
+
+ /// F2 key.
+ F2 = MaxCodePoint + ConsoleKey.F2,
+
+ /// F3 key.
+ F3 = MaxCodePoint + ConsoleKey.F3,
+
+ /// F4 key.
+ F4 = MaxCodePoint + ConsoleKey.F4,
+
+ /// F5 key.
+ F5 = MaxCodePoint + ConsoleKey.F5,
+
+ /// F6 key.
+ F6 = MaxCodePoint + ConsoleKey.F6,
+
+ /// F7 key.
+ F7 = MaxCodePoint + ConsoleKey.F7,
+
+ /// F8 key.
+ F8 = MaxCodePoint + ConsoleKey.F8,
+
+ /// F9 key.
+ F9 = MaxCodePoint + ConsoleKey.F9,
+
+ /// F10 key.
+ F10 = MaxCodePoint + ConsoleKey.F10,
+
+ /// F11 key.
+ F11 = MaxCodePoint + ConsoleKey.F11,
+
+ /// F12 key.
+ F12 = MaxCodePoint + ConsoleKey.F12,
+
+ /// F13 key.
+ F13 = MaxCodePoint + ConsoleKey.F13,
+
+ /// F14 key.
+ F14 = MaxCodePoint + ConsoleKey.F14,
+
+ /// F15 key.
+ F15 = MaxCodePoint + ConsoleKey.F15,
+
+ /// F16 key.
+ F16 = MaxCodePoint + ConsoleKey.F16,
+
+ /// F17 key.
+ F17 = MaxCodePoint + ConsoleKey.F17,
+
+ /// F18 key.
+ F18 = MaxCodePoint + ConsoleKey.F18,
+
+ /// F19 key.
+ F19 = MaxCodePoint + ConsoleKey.F19,
+
+ /// F20 key.
+ F20 = MaxCodePoint + ConsoleKey.F20,
+
+ /// F21 key.
+ F21 = MaxCodePoint + ConsoleKey.F21,
+
+ /// F22 key.
+ F22 = MaxCodePoint + ConsoleKey.F22,
+
+ /// F23 key.
+ F23 = MaxCodePoint + ConsoleKey.F23,
+
+ /// F24 key.
+ F24 = MaxCodePoint + ConsoleKey.F24
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs
deleted file mode 100644
index a5afbf2580..0000000000
--- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs
+++ /dev/null
@@ -1,1827 +0,0 @@
-//
-// 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 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);
-}
-
-internal class NetEvents : IDisposable
-{
- private readonly ManualResetEventSlim _inputReady = new (false);
- private CancellationTokenSource _inputReadyCancellationTokenSource;
- private readonly ManualResetEventSlim _waitForStart = new (false);
-
- //CancellationTokenSource _waitForStartCancellationTokenSource;
- private readonly ManualResetEventSlim _winChange = new (false);
- private readonly Queue _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)
- {
- return _inputQueue.Dequeue ();
- }
- }
-
- return null;
- }
-
- private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
- {
- // if there is a key available, return it without waiting
- // (or dispatching work to the thread queue)
- if (Console.KeyAvailable)
- {
- return Console.ReadKey (intercept);
- }
-
- while (!cancellationToken.IsCancellationRequested)
- {
- Task.Delay (100, cancellationToken).Wait (cancellationToken);
-
- if (Console.KeyAvailable)
- {
- return Console.ReadKey (intercept);
- }
- }
-
- cancellationToken.ThrowIfCancellationRequested ();
-
- return default (ConsoleKeyInfo);
- }
-
- private void ProcessInputQueue ()
- {
- while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
- {
- try
- {
- _waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
- }
- catch (OperationCanceledException)
- {
- return;
- }
-
- _waitForStart.Reset ();
-
- if (_inputQueue.Count == 0)
- {
- 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 ((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;
- 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);
-
- 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 ();
- }
- }
-
- /// Enqueue a window size event if the window size has changed.
- ///
- ///
- ///
- ///
- ///
- 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,
- out Point pos,
- out bool isReq,
- (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
- );
-
- if (isMouse)
- {
- foreach (MouseFlags mf in mouseFlags)
- {
- HandleMouseEvent (MapMouseFlags (mf), pos);
- }
-
- return;
- }
-
- if (isReq)
- {
- HandleRequestResponseEvent (c1Control, code, values, terminating);
-
- return;
- }
-
- HandleKeyboardEvent (newConsoleKeyInfo);
- }
-
- [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
- 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)
- {
- switch (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.
- case 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;
- }
-
- break;
-
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
- switch (values [0])
- {
- case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
- 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)
- );
-
- break;
- default:
- EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-
- break;
- }
-
- break;
- default:
- EnqueueRequestResponseEvent (c1Control, code, values, terminating);
-
- break;
- }
-
- _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
- };
- }
-
- /// Prints a ConsoleKeyInfoEx structure
- ///
- ///
- 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
- }
- }
-}
-
-internal class NetDriver : ConsoleDriver
-{
- 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;
- private NetMainLoop _mainLoopDriver;
- public bool IsWinPlatform { get; private set; }
- public NetWinVTConsole NetWinConsole { get; private set; }
-
- public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
- || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
-
- public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
- {
- var input = new InputResult
- {
- EventType = EventType.Key, ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control)
- };
-
- try
- {
- ProcessInput (input);
- }
- catch (OverflowException)
- { }
- }
-
- 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 ();
- }
-
- public override bool UpdateScreen ()
- {
- bool updated = false;
- if (RunningUnitTests
- || _winSizeChanging
- || Console.WindowHeight < 1
- || Contents.Length != Rows * Cols
- || Rows != Console.WindowHeight)
- {
- return updated;
- }
-
- 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 updated;
- }
-
- if (!_dirtyLines [row])
- {
- continue;
- }
-
- if (!SetCursorPosition (0, row))
- {
- return updated;
- }
-
- updated = true;
- _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 (var 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;
- }
-
- return updated;
- }
-
- internal override void End ()
- {
- if (IsWinPlatform)
- {
- NetWinConsole?.Cleanup ();
- }
-
- StopReportingMouseMoves ();
-
- 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 ();
- }
- }
-
- internal override MainLoop Init ()
- {
- PlatformID p = Environment.OSVersion.Platform;
-
- if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
- {
- IsWinPlatform = true;
-
- try
- {
- NetWinConsole = new NetWinVTConsole ();
- }
- 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 Attribute (Color.White, Color.Black);
-
- StartReportingMouseMoves ();
-
- _mainLoopDriver = new NetMainLoop (this);
- _mainLoopDriver.ProcessInput = ProcessInput;
-
-
- return new MainLoop (_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 Key (map));
- OnKeyUp (new Key (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 SizeChangedEventArgs (new (Cols, Rows)));
-
- break;
- case EventType.RequestResponse:
- break;
- case EventType.WindowPosition:
- break;
- default:
- throw new ArgumentOutOfRangeException ();
- }
- }
-
- #region Size and Position Handling
-
- private volatile bool _winSizeChanging;
-
- private void SetWindowPosition (int col, int row)
- {
- if (!RunningUnitTests)
- {
- Top = Console.WindowTop;
- Left = Console.WindowLeft;
- }
- else
- {
- Top = row;
- Left = col;
- }
- }
-
- public virtual 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 (Screen);
- }
- catch (ArgumentOutOfRangeException)
- {
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (Screen);
- }
- }
- else
- {
- Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
- }
-
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (Screen);
- }
-
- #endregion
-
- #region Color Handling
-
- // Cache the list of ConsoleColor values.
- [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
- private static readonly HashSet ConsoleColorValues = new (
- Enum.GetValues (typeof (ConsoleColor))
- .OfType ()
- .Select (c => (int)c)
- );
-
- // Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
- private static readonly Dictionary 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;
- }
-
- /////
- ///// In the NetDriver, colors are encoded as an int.
- ///// However, the foreground color is stored in the most significant 16 bits,
- ///// and the background color is stored in the least significant 16 bits.
- /////
- //public override Attribute MakeColor (Color foreground, Color background)
- //{
- // // Encode the colors into the int value.
- // return new Attribute (
- // platformColor: ((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
- // foreground: foreground,
- // background: background
- // );
- //}
-
- #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 (NetEvents.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 MouseEventArgs { Position = me.Position, Flags = mouseFlag };
- }
-
- #endregion Mouse Handling
-
- #region Keyboard Handling
-
- 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 ConsoleKeyInfo (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 ConsoleKeyInfo (
- 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 ConsoleKeyInfo (
- 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)((uint)keyInfo.KeyChar));
- }
-
- #endregion Keyboard Handling
-}
-
-///
-/// 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.
-///
-/// This implementation is used for NetDriver.
-internal class NetMainLoop : IMainLoopDriver
-{
- internal NetEvents _netEvents;
-
- /// Invoked when a Key is pressed.
- internal Action ProcessInput;
-
- private readonly ManualResetEventSlim _eventReady = new (false);
- private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
- private readonly Queue _resultQueue = new ();
- private readonly ManualResetEventSlim _waitForProbe = new (false);
- private readonly CancellationTokenSource _eventReadyTokenSource = new ();
- private MainLoop _mainLoop;
-
- /// Initializes the class with the console driver.
- /// Passing a consoleDriver is provided to capture windows resizing.
- /// The console driver used by this Net main loop.
- ///
- 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;
- 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)
- {
- ProcessInput?.Invoke (_resultQueue.Dequeue ().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 (!_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 ());
- }
-
- while (_resultQueue.Count > 0 && _resultQueue.Peek () is null)
- {
- _resultQueue.Dequeue ();
- }
-
- if (_resultQueue.Count > 0)
- {
- _eventReady.Set ();
- }
- }
- }
-}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
new file mode 100644
index 0000000000..91746a2180
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
@@ -0,0 +1,797 @@
+#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.Runtime.InteropServices;
+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 Suspend ()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Unix)
+ {
+ return;
+ }
+
+ StopReportingMouseMoves ();
+
+ if (!RunningUnitTests)
+ {
+ Console.ResetColor ();
+ Console.Clear ();
+
+ //Disable alternative screen buffer.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+ //Set cursor key to cursor.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_ShowCursor);
+
+ Platform.Suspend ();
+
+ //Enable alternative screen buffer.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+ SetContentsAsDirty ();
+ Refresh ();
+ }
+
+ StartReportingMouseMoves ();
+ }
+
+ public override bool UpdateScreen ()
+ {
+ bool updated = false;
+ if (RunningUnitTests
+ || _winSizeChanging
+ || Console.WindowHeight < 1
+ || Contents?.Length != Rows * Cols
+ || Rows != Console.WindowHeight)
+ {
+ return updated;
+ }
+
+ 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 updated;
+ }
+
+ if (!_dirtyLines! [row])
+ {
+ continue;
+ }
+
+ if (!SetCursorPosition (0, row))
+ {
+ return updated;
+ }
+
+ updated = true;
+ _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 (
+ AnsiEscapeSequenceRequestUtils.CSI_SetGraphicsRendition (
+ MapColors (
+ (ConsoleColor)attr.Background
+ .GetClosestNamedColor16 (),
+ false
+ ),
+ MapColors (
+ (ConsoleColor)attr.Foreground
+ .GetClosestNamedColor16 ())
+ )
+ );
+ }
+ else
+ {
+ output.Append (
+ AnsiEscapeSequenceRequestUtils.CSI_SetForegroundColorRGB (
+ attr.Foreground.R,
+ attr.Foreground.G,
+ attr.Foreground.B
+ )
+ );
+
+ output.Append (
+ AnsiEscapeSequenceRequestUtils.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 (var 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;
+ }
+
+ return updated;
+ }
+
+ #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 (AnsiEscapeSequenceRequestUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+ //Set cursor key to application.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.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;
+
+ if (!RunningUnitTests)
+ {
+ Task.Run (ProcessAnsiRequestHandler);
+ }
+
+ 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 = AnsiEscapeSequenceRequestUtils.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 ();
+
+
+
+ if (!RunningUnitTests)
+ {
+ Console.ResetColor ();
+
+ //Disable alternative screen buffer.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+ //Set cursor key to cursor.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.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 = "")]
+ //private static readonly HashSet ConsoleColorValues = new (
+ // Enum.GetValues (typeof (ConsoleColor))
+ // .OfType ()
+ // .Select (c => (int)c)
+ // );
+
+ // Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
+ private static readonly Dictionary _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 (AnsiEscapeSequenceRequestUtils.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 ? AnsiEscapeSequenceRequestUtils.CSI_ShowCursor : AnsiEscapeSequenceRequestUtils.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 (AnsiEscapeSequenceRequestUtils.CSI_EnableMouseEvents);
+ }
+ }
+
+ public void StopReportingMouseMoves ()
+ {
+ if (!RunningUnitTests)
+ {
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.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);
+ //}
+
+ #endregion Keyboard Handling
+
+ #region Low-Level DotNet tuff
+
+ ///
+ internal override void WriteRaw (string ansi)
+ {
+ Console.Out.Write (ansi);
+ Console.Out.Flush ();
+ }
+
+ private volatile bool _winSizeChanging;
+
+ private void SetWindowPosition (int col, int row)
+ {
+ if (!RunningUnitTests)
+ {
+ Top = Console.WindowTop;
+ Left = Console.WindowLeft;
+ }
+ else
+ {
+ Top = row;
+ Left = col;
+ }
+ }
+
+ public virtual 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 (Screen);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (Screen);
+ }
+ }
+ else
+ {
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SetTerminalWindowSize (Rows, Cols));
+ }
+
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (Screen);
+ }
+
+ #endregion Low-Level DotNet tuff
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
new file mode 100644
index 0000000000..05f5686675
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
@@ -0,0 +1,738 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+internal class NetEvents : IDisposable
+{
+ private readonly ManualResetEventSlim _inputReady = new (false);
+ private CancellationTokenSource? _inputReadyCancellationTokenSource;
+ private readonly Queue _inputQueue = new ();
+ private readonly ConsoleDriver _consoleDriver;
+ private ConsoleKeyInfo []? _cki;
+ private bool _isEscSeq;
+#if PROCESS_REQUEST
+ bool _neededProcessRequest;
+#endif
+ public NetEvents (ConsoleDriver consoleDriver)
+ {
+ _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+ _inputReadyCancellationTokenSource = new ();
+
+ Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+
+ Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
+ }
+
+ public InputResult? DequeueInput ()
+ {
+ while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false })
+ {
+ try
+ {
+ if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+ {
+ if (_inputQueue.Count == 0)
+ {
+ _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
+ }
+ }
+
+ if (_inputQueue.Count > 0)
+ {
+ return _inputQueue.Dequeue ();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ finally
+ {
+ if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ _inputReady.Reset ();
+ }
+ }
+
+#if PROCESS_REQUEST
+ _neededProcessRequest = false;
+#endif
+ }
+
+ 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);
+ }
+
+ // The delay must be here because it may have a request response after a while
+ // In WSL it takes longer for keys to be available.
+ Task.Delay (100, cancellationToken).Wait (cancellationToken);
+
+ ProcessResponse ();
+ }
+
+ cancellationToken.ThrowIfCancellationRequested ();
+
+ return default (ConsoleKeyInfo);
+ }
+
+ //internal bool _forceRead;
+ private int _retries;
+
+ private void ProcessResponse ()
+ {
+ if (!Console.KeyAvailable && AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && AnsiEscapeSequenceRequests.Statuses.Count > 0)
+ {
+ if (_retries > 1)
+ {
+ if (AnsiEscapeSequenceRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? seqReqStatus)
+ && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.AnsiEscapeSequenceResponse?.Response))
+ {
+ lock (seqReqStatus.AnsiRequest._responseLock)
+ {
+ AnsiEscapeSequenceRequests.Statuses.TryDequeue (out _);
+
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (null, seqReqStatus.AnsiRequest);
+ }
+ }
+
+ _retries = 0;
+ }
+ else
+ {
+ _retries++;
+ }
+ }
+ else
+ {
+ _retries = 0;
+ }
+ }
+
+ private void ProcessInputQueue ()
+ {
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ try
+ {
+ ConsoleKey key = 0;
+ ConsoleModifiers mod = 0;
+ ConsoleKeyInfo newConsoleKeyInfo = default;
+
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ ConsoleKeyInfo consoleKeyInfo;
+
+ try
+ {
+ consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ var ckiAlreadyResized = false;
+
+ if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is { })
+ {
+ ckiAlreadyResized = true;
+
+ _cki = AnsiEscapeSequenceRequestUtils.ResizeArray (consoleKeyInfo, _cki);
+ _cki = AnsiEscapeSequenceRequestUtils.InsertArray (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos, _cki);
+ AnsiEscapeSequenceRequestUtils.IncompleteCkInfos = null;
+
+ if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B')
+ {
+ _isEscSeq = true;
+ }
+ }
+
+ if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
+ || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
+ {
+ if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
+ {
+ _cki = AnsiEscapeSequenceRequestUtils.ResizeArray (
+ new (
+ (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.IsLetterOrDigit (consoleKeyInfo.KeyChar))
+ || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar))
+ || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar)))
+ {
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+ _cki = null;
+ _isEscSeq = false;
+ ProcessResponse ();
+
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+ }
+ else
+ {
+ newConsoleKeyInfo = consoleKeyInfo;
+
+ if (!ckiAlreadyResized)
+ {
+ _cki = AnsiEscapeSequenceRequestUtils.ResizeArray (consoleKeyInfo, _cki);
+ }
+
+ if (Console.KeyAvailable)
+ {
+ continue;
+ }
+
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki!, ref mod);
+ _cki = null;
+ _isEscSeq = false;
+ ProcessResponse ();
+ }
+
+ break;
+ }
+
+ if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
+ {
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+ _cki = null;
+ ProcessResponse ();
+
+ if (Console.KeyAvailable)
+ {
+ _cki = AnsiEscapeSequenceRequestUtils.ResizeArray (consoleKeyInfo, _cki);
+ }
+ else
+ {
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+ }
+
+ break;
+ }
+
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+
+ if (_retries > 0)
+ {
+ _retries = 0;
+ }
+
+ break;
+ }
+
+ if (_inputQueue.Count > 0)
+ {
+ _inputReady.Set ();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+
+ void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+ {
+ _inputQueue.Enqueue (
+ new ()
+ {
+ EventType = EventType.Key, ConsoleKeyInfo = AnsiEscapeSequenceRequestUtils.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
+ {
+ RequestWindowSize (_inputReadyCancellationTokenSource.Token);
+
+ if (_inputQueue.Count > 0)
+ {
+ _inputReady.Set ();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+
+ /// Enqueue a window size event if the window size has changed.
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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 ()
+ {
+ EventType = EventType.WindowSize, WindowSizeEvent = new () { 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
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ 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,
+ out Point pos,
+ out AnsiEscapeSequenceRequestStatus? 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);
+
+ return;
+ }
+
+ if (newConsoleKeyInfo != default)
+ {
+ HandleKeyboardEvent (newConsoleKeyInfo);
+ }
+ }
+
+ [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
+ 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 () { EventType = EventType.Mouse, MouseEvent = mouseEvent }
+ );
+ }
+
+ 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
+ })!;
+ }
+
+ /// Prints a ConsoleKeyInfoEx structure
+ ///
+ ///
+ 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;
+
+ _inputReady.Dispose ();
+
+ 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
+ }
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
new file mode 100644
index 0000000000..9ffc1298ee
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
@@ -0,0 +1,164 @@
+#nullable enable
+
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+///
+/// 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.
+///
+/// This implementation is used for NetDriver.
+internal class NetMainLoop : IMainLoopDriver
+{
+ internal NetEvents? _netEvents;
+
+ /// Invoked when a Key is pressed.
+ internal Action? ProcessInput;
+
+ private readonly ManualResetEventSlim _eventReady = new (false);
+ private readonly CancellationTokenSource _eventReadyTokenSource = new ();
+ private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
+ private readonly ConcurrentQueue _resultQueue = new ();
+ private MainLoop? _mainLoop;
+ bool IMainLoopDriver.ForceRead { get; set; }
+ ManualResetEventSlim IMainLoopDriver.WaitForInput { get; set; } = new (false);
+
+ /// Initializes the class with the console driver.
+ /// Passing a consoleDriver is provided to capture windows resizing.
+ /// The console driver used by this Net main loop.
+ ///
+ public NetMainLoop (ConsoleDriver consoleDriver)
+ {
+ ArgumentNullException.ThrowIfNull (consoleDriver);
+
+ if (!ConsoleDriver.RunningUnitTests)
+ {
+ _netEvents = new (consoleDriver);
+ }
+ }
+
+ void IMainLoopDriver.Setup (MainLoop mainLoop)
+ {
+ _mainLoop = mainLoop;
+
+ if (!ConsoleDriver.RunningUnitTests)
+ {
+ Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
+ }
+ }
+
+ void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+ bool IMainLoopDriver.EventsPending ()
+ {
+ ((IMainLoopDriver)this).WaitForInput.Set ();
+
+ if (_resultQueue.Count > 0 || _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);
+
+ _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
+
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+ return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return true;
+ }
+ finally
+ {
+ _eventReady.Reset ();
+ }
+
+ // If cancellation was requested then always return true
+ return true;
+ }
+
+ void IMainLoopDriver.Iteration ()
+ {
+ while (_resultQueue.TryDequeue (out NetEvents.InputResult inputRecords))
+ {
+ ProcessInput?.Invoke (inputRecords);
+ }
+ }
+
+ void IMainLoopDriver.TearDown ()
+ {
+ _inputHandlerTokenSource.Cancel ();
+ _inputHandlerTokenSource.Dispose ();
+ _eventReadyTokenSource.Cancel ();
+ _eventReadyTokenSource.Dispose ();
+
+ _eventReady.Dispose ();
+ ((IMainLoopDriver)this).WaitForInput.Dispose ();
+
+ _resultQueue.Clear ();
+ _netEvents?.Dispose ();
+ _netEvents = null;
+
+ _mainLoop = null;
+ }
+
+ private void NetInputHandler ()
+ {
+ while (_mainLoop is { })
+ {
+ try
+ {
+ if (!_inputHandlerTokenSource.IsCancellationRequested && !((IMainLoopDriver)this).ForceRead)
+ {
+ try
+ {
+ ((IMainLoopDriver)this).WaitForInput.Wait (_inputHandlerTokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ if (ex is OperationCanceledException or ObjectDisposedException)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ ((IMainLoopDriver)this).WaitForInput.Reset ();
+ }
+
+ ProcessInputQueue ();
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+
+ private void ProcessInputQueue ()
+ {
+ if (_resultQueue.Count == 0 || ((IMainLoopDriver)this).ForceRead)
+ {
+ NetEvents.InputResult? result = _netEvents!.DequeueInput ();
+
+ if (result.HasValue)
+ {
+ _resultQueue.Enqueue (result.Value);
+
+ _eventReady.Set ();
+ }
+ }
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
new file mode 100644
index 0000000000..98666f4609
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
@@ -0,0 +1,126 @@
+#nullable enable
+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);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
deleted file mode 100644
index 6c8c14b8cf..0000000000
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
+++ /dev/null
@@ -1,2540 +0,0 @@
-//
-// WindowsDriver.cs: Windows specific driver
-//
-
-// HACK:
-// WindowsConsole/Terminal has two issues:
-// 1) Tearing can occur when the console is resized.
-// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
-//
-// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
-// and instead check the console size every every 500ms in a thread in WidowsMainLoop.
-// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
-// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
-// still incorrect so we still need this hack.
-
-#define HACK_CHECK_WINCHANGED
-
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Text;
-using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
-using static Terminal.Gui.SpinnerStyle;
-
-namespace Terminal.Gui;
-
-internal class WindowsConsole
-{
- public const int STD_OUTPUT_HANDLE = -11;
- public const int STD_INPUT_HANDLE = -10;
-
- private readonly nint _inputHandle;
- private readonly nint _outputHandle;
- private nint _screenBuffer;
- private readonly uint _originalConsoleMode;
- private CursorVisibility? _initialCursorVisibility;
- private CursorVisibility? _currentCursorVisibility;
- private CursorVisibility? _pendingCursorVisibility;
- private readonly StringBuilder _stringBuilder = new (256 * 1024);
- private string _lastWrite = string.Empty;
-
- public WindowsConsole ()
- {
- _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
- _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
- _originalConsoleMode = ConsoleMode;
- uint newConsoleMode = _originalConsoleMode;
- newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
- newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
- newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
- ConsoleMode = newConsoleMode;
- }
-
- private CharInfo [] _originalStdOutChars;
-
- public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
- {
- //Debug.WriteLine ("WriteToConsole");
-
- if (_screenBuffer == nint.Zero)
- {
- ReadFromConsoleOutput (size, bufferSize, ref window);
- }
-
- var result = false;
-
- if (force16Colors)
- {
- var i = 0;
- CharInfo [] ci = new CharInfo [charInfoBuffer.Length];
-
- foreach (ExtendedCharInfo info in charInfoBuffer)
- {
- ci [i++] = new CharInfo
- {
- Char = new CharUnion { UnicodeChar = info.Char },
- Attributes =
- (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
- };
- }
-
- result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window);
- }
- else
- {
- _stringBuilder.Clear ();
-
- _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
- _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
-
- Attribute? prev = null;
-
- foreach (ExtendedCharInfo info in charInfoBuffer)
- {
- Attribute attr = info.Attribute;
-
- if (attr != prev)
- {
- prev = attr;
- _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
- _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
- }
-
- if (info.Char != '\x1b')
- {
- if (!info.Empty)
- {
- _stringBuilder.Append (info.Char);
- }
- }
- else
- {
- _stringBuilder.Append (' ');
- }
- }
-
- _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
- _stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
-
- var s = _stringBuilder.ToString ();
-
- // TODO: requires extensive testing if we go down this route
- // If console output has changed
- if (s != _lastWrite)
- {
- // supply console with the new content
- result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
- }
-
- _lastWrite = s;
-
- foreach (var sixel in Application.Sixel)
- {
- SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
- WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
- }
- }
-
- if (!result)
- {
- int err = Marshal.GetLastWin32Error ();
-
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
- }
-
- return result;
- }
-
- public bool WriteANSI (string ansi)
- {
- return WriteConsole (_screenBuffer, ansi, (uint)ansi.Length, out uint _, nint.Zero);
- }
-
- public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window)
- {
- _screenBuffer = CreateConsoleScreenBuffer (
- DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
- ShareMode.FileShareRead | ShareMode.FileShareWrite,
- nint.Zero,
- 1,
- nint.Zero
- );
-
- if (_screenBuffer == INVALID_HANDLE_VALUE)
- {
- int err = Marshal.GetLastWin32Error ();
-
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
- }
-
- SetInitialCursorVisibility ();
-
- if (!SetConsoleActiveScreenBuffer (_screenBuffer))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- _originalStdOutChars = new CharInfo [size.Height * size.Width];
-
- if (!ReadConsoleOutput (_screenBuffer, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
- }
-
- public bool SetCursorPosition (Coord position)
- {
- return SetConsoleCursorPosition (_screenBuffer, position);
- }
-
- public void SetInitialCursorVisibility ()
- {
- if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility))
- {
- _initialCursorVisibility = visibility;
- }
- }
-
- public bool GetCursorVisibility (out CursorVisibility visibility)
- {
- if (_screenBuffer == nint.Zero)
- {
- visibility = CursorVisibility.Invisible;
-
- return false;
- }
-
- if (!GetConsoleCursorInfo (_screenBuffer, out ConsoleCursorInfo info))
- {
- int err = Marshal.GetLastWin32Error ();
-
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
-
- visibility = CursorVisibility.Default;
-
- return false;
- }
-
- if (!info.bVisible)
- {
- visibility = CursorVisibility.Invisible;
- }
- else if (info.dwSize > 50)
- {
- visibility = CursorVisibility.Default;
- }
- else
- {
- visibility = CursorVisibility.Default;
- }
-
- return true;
- }
-
- public bool EnsureCursorVisibility ()
- {
- if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value))
- {
- _pendingCursorVisibility = null;
-
- return true;
- }
-
- return false;
- }
-
- public void ForceRefreshCursorVisibility ()
- {
- if (_currentCursorVisibility.HasValue)
- {
- _pendingCursorVisibility = _currentCursorVisibility;
- _currentCursorVisibility = null;
- }
- }
-
- public bool SetCursorVisibility (CursorVisibility visibility)
- {
- if (_initialCursorVisibility.HasValue == false)
- {
- _pendingCursorVisibility = visibility;
-
- return false;
- }
-
- if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility)
- {
- var info = new ConsoleCursorInfo
- {
- dwSize = (uint)visibility & 0x00FF,
- bVisible = ((uint)visibility & 0xFF00) != 0
- };
-
- if (!SetConsoleCursorInfo (_screenBuffer, ref info))
- {
- return false;
- }
-
- _currentCursorVisibility = visibility;
- }
-
- return true;
- }
-
- public void Cleanup ()
- {
- if (_initialCursorVisibility.HasValue)
- {
- SetCursorVisibility (_initialCursorVisibility.Value);
- }
-
- SetConsoleOutputWindow (out _);
-
- ConsoleMode = _originalConsoleMode;
-
- if (!SetConsoleActiveScreenBuffer (_outputHandle))
- {
- int err = Marshal.GetLastWin32Error ();
- Console.WriteLine ("Error: {0}", err);
- }
-
- if (_screenBuffer != nint.Zero)
- {
- CloseHandle (_screenBuffer);
- }
-
- _screenBuffer = nint.Zero;
- }
-
- internal Size GetConsoleBufferWindow (out Point position)
- {
- if (_screenBuffer == nint.Zero)
- {
- position = Point.Empty;
-
- return Size.Empty;
- }
-
- var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
- csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
- if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
- {
- //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
- position = Point.Empty;
-
- return Size.Empty;
- }
-
- Size sz = new (
- csbi.srWindow.Right - csbi.srWindow.Left + 1,
- csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
- position = new (csbi.srWindow.Left, csbi.srWindow.Top);
-
- return sz;
- }
-
- internal Size GetConsoleOutputWindow (out Point position)
- {
- var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
- csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
- if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- Size sz = new (
- csbi.srWindow.Right - csbi.srWindow.Left + 1,
- csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
- position = new (csbi.srWindow.Left, csbi.srWindow.Top);
-
- return sz;
- }
-
- internal Size SetConsoleWindow (short cols, short rows)
- {
- var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
- csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
- if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer);
- short newCols = Math.Min (cols, maxWinSize.X);
- short newRows = Math.Min (rows, maxWinSize.Y);
- csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1));
- csbi.srWindow = new SmallRect (0, 0, newCols, newRows);
- csbi.dwMaximumWindowSize = new Coord (newCols, newRows);
-
- if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
-
- if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
- {
- //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
- return new (cols, rows);
- }
-
- SetConsoleOutputWindow (csbi);
-
- return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
- }
-
- private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
- {
- if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
- }
-
- internal Size SetConsoleOutputWindow (out Point position)
- {
- if (_screenBuffer == nint.Zero)
- {
- position = Point.Empty;
-
- return Size.Empty;
- }
-
- var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
- csbi.cbSize = (uint)Marshal.SizeOf (csbi);
-
- if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- Size sz = new (
- csbi.srWindow.Right - csbi.srWindow.Left + 1,
- Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0));
- position = new (csbi.srWindow.Left, csbi.srWindow.Top);
- SetConsoleOutputWindow (csbi);
- var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
-
- if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
- {
- throw new Win32Exception (Marshal.GetLastWin32Error ());
- }
-
- return sz;
- }
-
- private uint ConsoleMode
- {
- get
- {
- GetConsoleMode (_inputHandle, out uint v);
-
- return v;
- }
- set => SetConsoleMode (_inputHandle, value);
- }
-
- [Flags]
- public enum ConsoleModes : uint
- {
- EnableProcessedInput = 1,
- EnableMouseInput = 16,
- EnableQuickEditMode = 64,
- EnableExtendedFlags = 128
- }
-
- [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
- public struct KeyEventRecord
- {
- [FieldOffset (0)]
- [MarshalAs (UnmanagedType.Bool)]
- public bool bKeyDown;
-
- [FieldOffset (4)]
- [MarshalAs (UnmanagedType.U2)]
- public ushort wRepeatCount;
-
- [FieldOffset (6)]
- [MarshalAs (UnmanagedType.U2)]
- public VK wVirtualKeyCode;
-
- [FieldOffset (8)]
- [MarshalAs (UnmanagedType.U2)]
- public ushort wVirtualScanCode;
-
- [FieldOffset (10)]
- public char UnicodeChar;
-
- [FieldOffset (12)]
- [MarshalAs (UnmanagedType.U4)]
- public ControlKeyState dwControlKeyState;
-
- public readonly override string ToString ()
- {
- return
- $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]";
- }
- }
-
- [Flags]
- public enum ButtonState
- {
- NoButtonPressed = 0,
- Button1Pressed = 1,
- Button2Pressed = 4,
- Button3Pressed = 8,
- Button4Pressed = 16,
- RightmostButtonPressed = 2
- }
-
- [Flags]
- public enum ControlKeyState
- {
- NoControlKeyPressed = 0,
- RightAltPressed = 1,
- LeftAltPressed = 2,
- RightControlPressed = 4,
- LeftControlPressed = 8,
- ShiftPressed = 16,
- NumlockOn = 32,
- ScrolllockOn = 64,
- CapslockOn = 128,
- EnhancedKey = 256
- }
-
- [Flags]
- public enum EventFlags
- {
- NoEvent = 0,
- MouseMoved = 1,
- DoubleClick = 2,
- MouseWheeled = 4,
- MouseHorizontalWheeled = 8
- }
-
- [StructLayout (LayoutKind.Explicit)]
- public struct MouseEventRecord
- {
- [FieldOffset (0)]
- public Coord MousePosition;
-
- [FieldOffset (4)]
- public ButtonState ButtonState;
-
- [FieldOffset (8)]
- public ControlKeyState ControlKeyState;
-
- [FieldOffset (12)]
- public EventFlags EventFlags;
-
- public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; }
- }
-
- public struct WindowBufferSizeRecord
- {
- public Coord _size;
-
- public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); }
-
- public readonly override string ToString () { return $"[WindowBufferSize{_size}"; }
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct MenuEventRecord
- {
- public uint dwCommandId;
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct FocusEventRecord
- {
- public uint bSetFocus;
- }
-
- public enum EventType : ushort
- {
- Focus = 0x10,
- Key = 0x1,
- Menu = 0x8,
- Mouse = 2,
- WindowBufferSize = 4
- }
-
- [StructLayout (LayoutKind.Explicit)]
- public struct InputRecord
- {
- [FieldOffset (0)]
- public EventType EventType;
-
- [FieldOffset (4)]
- public KeyEventRecord KeyEvent;
-
- [FieldOffset (4)]
- public MouseEventRecord MouseEvent;
-
- [FieldOffset (4)]
- public WindowBufferSizeRecord WindowBufferSizeEvent;
-
- [FieldOffset (4)]
- public MenuEventRecord MenuEvent;
-
- [FieldOffset (4)]
- public FocusEventRecord FocusEvent;
-
- public readonly override string ToString ()
- {
- return EventType switch
- {
- EventType.Focus => FocusEvent.ToString (),
- EventType.Key => KeyEvent.ToString (),
- EventType.Menu => MenuEvent.ToString (),
- EventType.Mouse => MouseEvent.ToString (),
- EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
- _ => "Unknown event type: " + EventType
- };
- }
- }
-
- [Flags]
- private enum ShareMode : uint
- {
- FileShareRead = 1,
- FileShareWrite = 2
- }
-
- [Flags]
- private enum DesiredAccess : uint
- {
- GenericRead = 2147483648,
- GenericWrite = 1073741824
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct ConsoleScreenBufferInfo
- {
- public Coord dwSize;
- public Coord dwCursorPosition;
- public ushort wAttributes;
- public SmallRect srWindow;
- public Coord dwMaximumWindowSize;
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct Coord
- {
- public short X;
- public short Y;
-
- public Coord (short x, short y)
- {
- X = x;
- Y = y;
- }
-
- public readonly override string ToString () { return $"({X},{Y})"; }
- }
-
- [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
- public struct CharUnion
- {
- [FieldOffset (0)]
- public char UnicodeChar;
-
- [FieldOffset (0)]
- public byte AsciiChar;
- }
-
- [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
- public struct CharInfo
- {
- [FieldOffset (0)]
- public CharUnion Char;
-
- [FieldOffset (2)]
- public ushort Attributes;
- }
-
- public struct ExtendedCharInfo
- {
- public char Char { get; set; }
- public Attribute Attribute { get; set; }
- public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences
-
- public ExtendedCharInfo (char character, Attribute attribute)
- {
- Char = character;
- Attribute = attribute;
- Empty = false;
- }
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct SmallRect
- {
- public short Left;
- public short Top;
- public short Right;
- public short Bottom;
-
- public SmallRect (short left, short top, short right, short bottom)
- {
- Left = left;
- Top = top;
- Right = right;
- Bottom = bottom;
- }
-
- public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; }
-
- public static void Update (ref SmallRect rect, short col, short row)
- {
- if (rect.Left == -1)
- {
- rect.Left = rect.Right = col;
- rect.Bottom = rect.Top = row;
-
- return;
- }
-
- if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom)
- {
- return;
- }
-
- if (col < rect.Left)
- {
- rect.Left = col;
- }
-
- if (col > rect.Right)
- {
- rect.Right = col;
- }
-
- if (row < rect.Top)
- {
- rect.Top = row;
- }
-
- if (row > rect.Bottom)
- {
- rect.Bottom = row;
- }
- }
-
- public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; }
- }
-
- [StructLayout (LayoutKind.Sequential)]
- public struct ConsoleKeyInfoEx
- {
- public ConsoleKeyInfo ConsoleKeyInfo;
- public bool CapsLock;
- public bool NumLock;
- public bool ScrollLock;
-
- public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
- {
- ConsoleKeyInfo = consoleKeyInfo;
- CapsLock = capslock;
- NumLock = numlock;
- ScrollLock = scrolllock;
- }
-
- ///
- /// Prints a ConsoleKeyInfoEx structure
- ///
- ///
- ///
- public readonly string ToString (ConsoleKeyInfoEx ex)
- {
- var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar);
- var sb = new StringBuilder ();
- sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})");
- sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
- sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
- sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
- sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) ");
- sb.Append (ex.CapsLock ? "caps," : string.Empty);
- sb.Append (ex.NumLock ? "num," : string.Empty);
- sb.Append (ex.ScrollLock ? "scroll," : string.Empty);
- string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
-
- return $"[ConsoleKeyInfoEx({s})]";
- }
- }
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern nint GetStdHandle (int nStdHandle);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool CloseHandle (nint handle);
-
- [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
- public static extern bool ReadConsoleInput (
- nint hConsoleInput,
- nint lpBuffer,
- uint nLength,
- out uint lpNumberOfEventsRead
- );
-
- [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
- private static extern bool ReadConsoleOutput (
- nint hConsoleOutput,
- [Out] CharInfo [] lpBuffer,
- Coord dwBufferSize,
- Coord dwBufferCoord,
- ref SmallRect lpReadRegion
- );
-
- // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput
- [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)]
- private static extern bool WriteConsoleOutput (
- nint hConsoleOutput,
- CharInfo [] lpBuffer,
- Coord dwBufferSize,
- Coord dwBufferCoord,
- ref SmallRect lpWriteRegion
- );
-
- [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
- private static extern bool WriteConsole (
- nint hConsoleOutput,
- string lpbufer,
- uint NumberOfCharsToWriten,
- out uint lpNumberOfCharsWritten,
- nint lpReserved
- );
-
- [DllImport ("kernel32.dll")]
- private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
-
- [StructLayout (LayoutKind.Sequential)]
- public struct ConsoleCursorInfo
- {
- ///
- /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100.
- /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal
- /// line at the bottom of the cell.
- ///
- public uint dwSize;
- public bool bVisible;
- }
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo);
-
- [DllImport ("kernel32.dll")]
- private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
-
- [DllImport ("kernel32.dll")]
- private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern nint CreateConsoleScreenBuffer (
- DesiredAccess dwDesiredAccess,
- ShareMode dwShareMode,
- nint secutiryAttributes,
- uint flags,
- nint screenBufferData
- );
-
- internal static nint INVALID_HANDLE_VALUE = new (-1);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleActiveScreenBuffer (nint Handle);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents);
-
- internal uint GetNumberOfConsoleInputEvents ()
- {
- if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents))
- {
- Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
-
- return 0;
- }
-
- return numOfEvents;
- }
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool FlushConsoleInputBuffer (nint handle);
-
- internal void FlushConsoleInputBuffer ()
- {
- if (!FlushConsoleInputBuffer (_inputHandle))
- {
- Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
- }
- }
-
- public InputRecord [] ReadConsoleInput ()
- {
- const int bufferSize = 1;
- nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize);
-
- try
- {
- ReadConsoleInput (
- _inputHandle,
- pRecord,
- bufferSize,
- out uint numberEventsRead);
-
- return numberEventsRead == 0
- ? null
- : new [] { Marshal.PtrToStructure (pRecord) };
- }
- catch (Exception)
- {
- return null;
- }
- finally
- {
- Marshal.FreeHGlobal (pRecord);
- }
- }
-
-#if false // Not needed on the constructor. Perhaps could be used on resizing. To study.
- [DllImport ("kernel32.dll", ExactSpelling = true)]
- static extern IntPtr GetConsoleWindow ();
-
- [DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
- static extern bool ShowWindow (IntPtr hWnd, int nCmdShow);
-
- public const int HIDE = 0;
- public const int MAXIMIZE = 3;
- public const int MINIMIZE = 6;
- public const int RESTORE = 9;
-
- internal void ShowWindow (int state)
- {
- IntPtr thisConsole = GetConsoleWindow ();
- ShowWindow (thisConsole, state);
- }
-#endif
-
- // See: https://github.com/gui-cs/Terminal.Gui/issues/357
-
- [StructLayout (LayoutKind.Sequential)]
- public struct CONSOLE_SCREEN_BUFFER_INFOEX
- {
- public uint cbSize;
- public Coord dwSize;
- public Coord dwCursorPosition;
- public ushort wAttributes;
- public SmallRect srWindow;
- public Coord dwMaximumWindowSize;
- public ushort wPopupAttributes;
- public bool bFullscreenSupported;
-
- [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)]
- public COLORREF [] ColorTable;
- }
-
- [StructLayout (LayoutKind.Explicit, Size = 4)]
- public struct COLORREF
- {
- public COLORREF (byte r, byte g, byte b)
- {
- Value = 0;
- R = r;
- G = g;
- B = b;
- }
-
- public COLORREF (uint value)
- {
- R = 0;
- G = 0;
- B = 0;
- Value = value & 0x00FFFFFF;
- }
-
- [FieldOffset (0)]
- public byte R;
-
- [FieldOffset (1)]
- public byte G;
-
- [FieldOffset (2)]
- public byte B;
-
- [FieldOffset (0)]
- public uint Value;
- }
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern bool SetConsoleWindowInfo (
- nint hConsoleOutput,
- bool bAbsolute,
- [In] ref SmallRect lpConsoleWindow
- );
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern Coord GetLargestConsoleWindowSize (
- nint hConsoleOutput
- );
-}
-
-internal class WindowsDriver : ConsoleDriver
-{
- private readonly bool _isWindowsTerminal;
-
- private WindowsConsole.SmallRect _damageRegion;
- private bool _isButtonDoubleClicked;
- private bool _isButtonPressed;
- private bool _isButtonReleased;
- private bool _isOneFingerDoubleClicked;
-
- private WindowsConsole.ButtonState? _lastMouseButtonPressed;
- private WindowsMainLoop _mainLoopDriver;
- private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
- private Point? _point;
- private Point _pointMove;
- private bool _processButtonClick;
-
- public WindowsDriver ()
- {
- if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- {
- WinConsole = new WindowsConsole ();
-
- // otherwise we're probably running in unit tests
- Clipboard = new WindowsClipboard ();
- }
- else
- {
- Clipboard = new FakeDriver.FakeClipboard ();
- }
-
- // TODO: if some other Windows-based terminal supports true color, update this logic to not
- // force 16color mode (.e.g ConEmu which really doesn't work well at all).
- _isWindowsTerminal = _isWindowsTerminal =
- Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
-
- if (!_isWindowsTerminal)
- {
- Force16Colors = true;
- }
- }
-
- public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
-
- public WindowsConsole WinConsole { get; private set; }
-
- public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
- {
- if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
- {
- return keyEvent;
- }
-
- var mod = new ConsoleModifiers ();
-
- if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
- {
- mod |= ConsoleModifiers.Shift;
- }
-
- if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
- || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
- {
- mod |= ConsoleModifiers.Alt;
- }
-
- if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)
- || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed))
- {
- mod |= ConsoleModifiers.Control;
- }
-
- var cKeyInfo = new ConsoleKeyInfo (
- keyEvent.UnicodeChar,
- (ConsoleKey)keyEvent.wVirtualKeyCode,
- mod.HasFlag (ConsoleModifiers.Shift),
- mod.HasFlag (ConsoleModifiers.Alt),
- mod.HasFlag (ConsoleModifiers.Control));
- cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
- uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo);
-
- return new WindowsConsole.KeyEventRecord
- {
- UnicodeChar = cKeyInfo.KeyChar,
- bKeyDown = keyEvent.bKeyDown,
- dwControlKeyState = keyEvent.dwControlKeyState,
- wRepeatCount = keyEvent.wRepeatCount,
- wVirtualKeyCode = (VK)cKeyInfo.Key,
- wVirtualScanCode = (ushort)scanCode
- };
- }
-
- public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; }
-
- public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
- {
- var input = new WindowsConsole.InputRecord
- {
- EventType = WindowsConsole.EventType.Key
- };
-
- var keyEvent = new WindowsConsole.KeyEventRecord
- {
- bKeyDown = true
- };
- var controlKey = new WindowsConsole.ControlKeyState ();
-
- if (shift)
- {
- controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
- keyEvent.UnicodeChar = '\0';
- keyEvent.wVirtualKeyCode = VK.SHIFT;
- }
-
- if (alt)
- {
- controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
- controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
- keyEvent.UnicodeChar = '\0';
- keyEvent.wVirtualKeyCode = VK.MENU;
- }
-
- if (control)
- {
- controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
- controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
- keyEvent.UnicodeChar = '\0';
- keyEvent.wVirtualKeyCode = VK.CONTROL;
- }
-
- keyEvent.dwControlKeyState = controlKey;
-
- input.KeyEvent = keyEvent;
-
- if (shift || alt || control)
- {
- ProcessInput (input);
- }
-
- keyEvent.UnicodeChar = keyChar;
-
- //if ((uint)key < 255) {
- // keyEvent.wVirtualKeyCode = (ushort)key;
- //} else {
- // keyEvent.wVirtualKeyCode = '\0';
- //}
- keyEvent.wVirtualKeyCode = (VK)key;
-
- input.KeyEvent = keyEvent;
-
- try
- {
- ProcessInput (input);
- }
- catch (OverflowException)
- { }
- finally
- {
- keyEvent.bKeyDown = false;
- input.KeyEvent = keyEvent;
- ProcessInput (input);
- }
- }
-
-
- #region Not Implemented
-
- public override void Suspend () { throw new NotImplementedException (); }
-
- #endregion
-
- public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
- {
- WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
-
- bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0;
- bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0;
- bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0;
- bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0;
- bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0;
- bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0;
-
- var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
-
- return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock);
- }
-
- #region Cursor Handling
-
- private CursorVisibility? _cachedCursorVisibility;
-
- public override void UpdateCursor ()
- {
- if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows)
- {
- GetCursorVisibility (out CursorVisibility cursorVisibility);
- _cachedCursorVisibility = cursorVisibility;
- SetCursorVisibility (CursorVisibility.Invisible);
-
- return;
- }
-
- var position = new WindowsConsole.Coord
- {
- X = (short)Col,
- Y = (short)Row
- };
-
- if (Force16Colors)
- {
- WinConsole?.SetCursorPosition (position);
- }
- else
- {
- var sb = new StringBuilder ();
- sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
- WinConsole?.WriteANSI (sb.ToString ());
- }
-
- if (_cachedCursorVisibility is { })
- {
- SetCursorVisibility (_cachedCursorVisibility.Value);
- }
- //EnsureCursorVisibility ();
- }
-
- ///
- public override bool GetCursorVisibility (out CursorVisibility visibility)
- {
- if (WinConsole is { })
- {
- return WinConsole.GetCursorVisibility (out visibility);
- }
-
- visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
-
- return true;
- }
-
- ///
- public override bool SetCursorVisibility (CursorVisibility visibility)
- {
- _cachedCursorVisibility = visibility;
-
- if (Force16Colors)
- {
- return WinConsole is null || WinConsole.SetCursorVisibility (visibility);
- }
- else
- {
- var sb = new StringBuilder ();
- sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
- return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
- }
- }
-
- ///
- public override bool EnsureCursorVisibility ()
- {
- if (Force16Colors)
- {
- return WinConsole is null || WinConsole.EnsureCursorVisibility ();
- }
- else
- {
- var sb = new StringBuilder ();
- sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
- return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
- }
-
- //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 Cursor Handling
-
- public override bool UpdateScreen ()
- {
- bool updated = false;
- Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows);
-
- if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows))
- {
- return updated;
- }
-
- var bufferCoords = new WindowsConsole.Coord
- {
- X = (short)Cols, //Clip.Width,
- Y = (short)Rows, //Clip.Height
- };
-
- for (var row = 0; row < Rows; row++)
- {
- if (!_dirtyLines [row])
- {
- continue;
- }
-
- _dirtyLines [row] = false;
- updated = true;
-
- for (var col = 0; col < Cols; col++)
- {
- int position = row * Cols + col;
- _outputBuffer [position].Attribute = Contents [row, col].Attribute.GetValueOrDefault ();
-
- if (Contents [row, col].IsDirty == false)
- {
- _outputBuffer [position].Empty = true;
- _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
-
- continue;
- }
-
- _outputBuffer [position].Empty = false;
-
- if (Contents [row, col].Rune.IsBmp)
- {
- _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value;
- }
- else
- {
- //_outputBuffer [position].Empty = true;
- _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
-
- if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols)
- {
- // TODO: This is a hack to deal with non-BMP and wide characters.
- col++;
- position = row * Cols + col;
- _outputBuffer [position].Empty = false;
- _outputBuffer [position].Char = ' ';
- }
- }
- }
- }
-
- _damageRegion = new WindowsConsole.SmallRect
- {
- Top = 0,
- Left = 0,
- Bottom = (short)Rows,
- Right = (short)Cols
- };
-
- if (!RunningUnitTests
- && WinConsole != null
- && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors))
- {
- int err = Marshal.GetLastWin32Error ();
-
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
- }
-
- WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
-
- return updated;
- }
-
- internal override void End ()
- {
- if (_mainLoopDriver is { })
- {
-#if HACK_CHECK_WINCHANGED
-
- _mainLoopDriver.WinChanged -= ChangeWin;
-#endif
- }
-
- _mainLoopDriver = null;
-
- WinConsole?.Cleanup ();
- WinConsole = null;
-
- if (!RunningUnitTests && _isWindowsTerminal)
- {
- // Disable alternative screen buffer.
- Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
- }
- }
-
- internal override MainLoop Init ()
- {
- _mainLoopDriver = new WindowsMainLoop (this);
-
- if (!RunningUnitTests)
- {
- try
- {
- if (WinConsole is { })
- {
- // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
- // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
- Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
- Cols = winSize.Width;
- Rows = winSize.Height;
- OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
- }
-
- WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
-
- if (_isWindowsTerminal)
- {
- Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
- }
- }
- catch (Win32Exception e)
- {
- // We are being run in an environment that does not support a console
- // such as a unit test, or a pipe.
- Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}");
- WinConsole = null;
- }
- }
-
- CurrentAttribute = new Attribute (Color.White, Color.Black);
-
- _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (Screen);
-
- _damageRegion = new WindowsConsole.SmallRect
- {
- Top = 0,
- Left = 0,
- Bottom = (short)Rows,
- Right = (short)Cols
- };
-
- ClearContents ();
-
-#if HACK_CHECK_WINCHANGED
- _mainLoopDriver.WinChanged = ChangeWin;
-#endif
-
- WinConsole?.SetInitialCursorVisibility ();
- return new MainLoop (_mainLoopDriver);
- }
-
- internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
- {
- switch (inputEvent.EventType)
- {
- case WindowsConsole.EventType.Key:
- if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet)
- {
- // Used to pass Unicode characters as if they were keystrokes.
- // The VK_PACKET key is the low word of a 32-bit
- // Virtual Key value used for non-keyboard input methods.
- inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
- }
-
- WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent);
-
- //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
-
- KeyCode map = MapKey (keyInfo);
-
- if (map == KeyCode.Null)
- {
- break;
- }
-
- if (inputEvent.KeyEvent.bKeyDown)
- {
- // Avoid sending repeat key down events
- OnKeyDown (new Key (map));
- }
- else
- {
- OnKeyUp (new Key (map));
- }
-
- break;
-
- case WindowsConsole.EventType.Mouse:
- MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
-
- if (me is null || me.Flags == MouseFlags.None)
- {
- break;
- }
-
- OnMouseEvent (me);
-
- if (_processButtonClick)
- {
- OnMouseEvent (new ()
- {
- Position = me.Position,
- Flags = ProcessButtonClick (inputEvent.MouseEvent)
- });
- }
-
- break;
-
- case WindowsConsole.EventType.Focus:
- break;
-
-#if !HACK_CHECK_WINCHANGED
- case WindowsConsole.EventType.WindowBufferSize:
-
- Cols = inputEvent.WindowBufferSizeEvent._size.X;
- Rows = inputEvent.WindowBufferSizeEvent._size.Y;
-
- ResizeScreen ();
- ClearContents ();
- TerminalResized.Invoke ();
- break;
-#endif
- }
- }
-
-#if HACK_CHECK_WINCHANGED
- private void ChangeWin (object s, SizeChangedEventArgs e)
- {
- if (e.Size is null)
- {
- return;
- }
-
- int w = e.Size.Value.Width;
-
- if (w == Cols - 3 && e.Size.Value.Height < Rows)
- {
- w += 3;
- }
-
- Left = 0;
- Top = 0;
- Cols = e.Size.Value.Width;
- Rows = e.Size.Value.Height;
-
- if (!RunningUnitTests)
- {
- Size newSize = WinConsole.SetConsoleWindow (
- (short)Math.Max (w, 16),
- (short)Math.Max (e.Size.Value.Height, 0));
-
- Cols = newSize.Width;
- Rows = newSize.Height;
- }
-
- ResizeScreen ();
- ClearContents ();
- OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
- }
-#endif
-
- private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
- {
- ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
-
- switch (keyInfo.Key)
- {
- case ConsoleKey.D0:
- case ConsoleKey.D1:
- case ConsoleKey.D2:
- case ConsoleKey.D3:
- case ConsoleKey.D4:
- case ConsoleKey.D5:
- case ConsoleKey.D6:
- case ConsoleKey.D7:
- case ConsoleKey.D8:
- case ConsoleKey.D9:
- case ConsoleKey.NumPad0:
- case ConsoleKey.NumPad1:
- case ConsoleKey.NumPad2:
- case ConsoleKey.NumPad3:
- case ConsoleKey.NumPad4:
- case ConsoleKey.NumPad5:
- case ConsoleKey.NumPad6:
- case ConsoleKey.NumPad7:
- case ConsoleKey.NumPad8:
- case ConsoleKey.NumPad9:
- 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:
- case ConsoleKey.Multiply:
- case ConsoleKey.Add:
- case ConsoleKey.Separator:
- case ConsoleKey.Subtract:
- case ConsoleKey.Decimal:
- case ConsoleKey.Divide:
- case ConsoleKey.OemPeriod:
- case ConsoleKey.OemComma:
- case ConsoleKey.OemPlus:
- case ConsoleKey.OemMinus:
- // These virtual key codes are mapped differently depending on the keyboard layout in use.
- // We use the Win32 API to map them to the correct character.
- uint mapResult = MapVKtoChar ((VK)keyInfo.Key);
-
- if (mapResult == 0)
- {
- // There is no mapping - this should not happen
- Debug.Assert (mapResult != 0, $@"Unable to map the virtual key code {keyInfo.Key}.");
-
- return KeyCode.Null;
- }
-
- // An un-shifted character value is in the low order word of the return value.
- var mappedChar = (char)(mapResult & 0x0000FFFF);
-
- if (keyInfo.KeyChar == 0)
- {
- // If the keyChar is 0, keyInfo.Key value is not a printable character.
-
- // Dead keys (diacritics) are indicated by setting the top bit of the return value.
- if ((mapResult & 0x80000000) != 0)
- {
- // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
- // Option 1: Throw it out.
- // - Apps will never see the dead keys
- // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
- // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
- // - If user presses dead key again, the right thing happens (app will see `~~`)
- // - This is what Notepad etc... appear to do
- // Option 2: Expand the API to indicate the KeyCode is a dead key
- // - Enables apps to do their own dead key processing
- // - Adds complexity; no dev has asked for this (yet).
- // We choose Option 1 for now.
- return KeyCode.Null;
-
- // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported.
- // Sadly, the charVal is just the deadkey and subsequent key events do not contain
- // any info that the previous event was a deadkey.
- // Note WT does not support Ctrl-Deadkey either.
- }
-
- if (keyInfo.Modifiers != 0)
- {
- // These Oem keys have well-defined chars. We ensure the representative char is used.
- // If we don't do this, then on some keyboard layouts the wrong char is
- // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
- // for key persistence ("Ctrl++" vs. "Ctrl+=").
- mappedChar = keyInfo.Key switch
- {
- ConsoleKey.OemPeriod => '.',
- ConsoleKey.OemComma => ',',
- ConsoleKey.OemPlus => '+',
- ConsoleKey.OemMinus => '-',
- _ => mappedChar
- };
- }
-
- // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down
- // we should keep it
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
- }
-
- // KeyChar is printable
- if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
- {
- // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar
- return (KeyCode)keyInfo.KeyChar;
- }
-
- if (keyInfo.Modifiers != ConsoleModifiers.Shift)
- {
- // If Shift wasn't down we don't need to do anything but return the mappedChar
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
- }
-
- // 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);
- }
-
- // A..Z are special cased:
- // - Alone, they represent lowercase a...z
- // - With ShiftMask they are A..Z
- // - If CapsLock is on the above is reversed.
- // - If Alt and/or Ctrl are present, treat as upper case
- if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
- {
- if (keyInfo.KeyChar == 0)
- {
- // KeyChar is not printable - possibly an AltGr key?
- // AltGr support - AltGr is equivalent to Ctrl+Alt
- if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
- {
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
- }
- }
-
- if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
- {
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
- }
-
- if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock)
- {
- // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask
- if (char.IsUpper (keyInfo.KeyChar))
- {
- return (KeyCode)(uint)keyInfo.Key | KeyCode.ShiftMask;
- }
- }
-
- // Return the Key (not KeyChar!)
- return (KeyCode)keyInfo.Key;
- }
-
- // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
- if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
- {
- // If the key is JUST a modifier, return it as just that key
- if (keyInfo.Key == (ConsoleKey)VK.SHIFT)
- { // Shift 16
- return KeyCode.ShiftMask;
- }
-
- if (keyInfo.Key == (ConsoleKey)VK.CONTROL)
- { // Ctrl 17
- return KeyCode.CtrlMask;
- }
-
- if (keyInfo.Key == (ConsoleKey)VK.MENU)
- { // Alt 18
- return KeyCode.AltMask;
- }
-
- if (keyInfo.KeyChar == 0)
- {
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
- }
-
- if (keyInfo.Key != ConsoleKey.None)
- {
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
- }
-
- return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
- }
-
- // Handle control keys (e.g. CursorUp)
- if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
- {
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
- }
-
- return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
- }
-
- private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent)
- {
- MouseFlags mouseFlag = 0;
-
- switch (_lastMouseButtonPressed)
- {
- case WindowsConsole.ButtonState.Button1Pressed:
- mouseFlag = MouseFlags.Button1Clicked;
-
- break;
-
- case WindowsConsole.ButtonState.Button2Pressed:
- mouseFlag = MouseFlags.Button2Clicked;
-
- break;
-
- case WindowsConsole.ButtonState.RightmostButtonPressed:
- mouseFlag = MouseFlags.Button3Clicked;
-
- break;
- }
-
- _point = new Point
- {
- X = mouseEvent.MousePosition.X,
- Y = mouseEvent.MousePosition.Y
- };
- _lastMouseButtonPressed = null;
- _isButtonReleased = false;
- _processButtonClick = false;
- _point = null;
-
- return mouseFlag;
- }
-
- private async Task ProcessButtonDoubleClickedAsync ()
- {
- await Task.Delay (200);
- _isButtonDoubleClicked = false;
- _isOneFingerDoubleClicked = false;
-
- //buttonPressedCount = 0;
- }
-
- private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
- {
- // When a user presses-and-holds, start generating pressed events every `startDelay`
- // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
- const int startDelay = 500;
- const int iterationsUntilFast = 4;
- const int fastDelay = 50;
-
- int iterations = 0;
- int delay = startDelay;
- while (_isButtonPressed)
- {
- // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
- View view = Application.WantContinuousButtonPressedView;
-
- if (view is null)
- {
- break;
- }
-
- if (iterations++ >= iterationsUntilFast)
- {
- delay = fastDelay;
- }
- await Task.Delay (delay);
-
- var me = new MouseEventArgs
- {
- ScreenPosition = _pointMove,
- Flags = mouseFlag
- };
-
- //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
- if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
- {
- // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
- Application.Invoke (() => OnMouseEvent (me));
- }
- }
- }
-
- private void ResizeScreen ()
- {
- _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (Screen);
-
- _damageRegion = new WindowsConsole.SmallRect
- {
- Top = 0,
- Left = 0,
- Bottom = (short)Rows,
- Right = (short)Cols
- };
- _dirtyLines = new bool [Rows];
-
- WinConsole?.ForceRefreshCursorVisibility ();
- }
-
- private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag)
- {
- if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)
- || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed))
- {
- mouseFlag |= MouseFlags.ButtonCtrl;
- }
-
- if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
- {
- mouseFlag |= MouseFlags.ButtonShift;
- }
-
- if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
- || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
- {
- mouseFlag |= MouseFlags.ButtonAlt;
- }
-
- return mouseFlag;
- }
-
- [CanBeNull]
- private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
- {
- var mouseFlag = MouseFlags.AllEvents;
-
- //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}");
-
- if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
- {
- // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
- Application.MainLoop.AddIdle (
- () =>
- {
- Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
-
- return false;
- });
- }
-
- // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
- // This will tell when a mouse button is pressed. When the button is released this event will
- // be fired with its bit set to 0. So when the button is up ButtonState will be 0.
- // To map to the correct driver events we save the last pressed mouse button, so we can
- // map to the correct clicked event.
- if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0)
- {
- _lastMouseButtonPressed = null;
-
- //isButtonPressed = false;
- _isButtonReleased = false;
- }
-
- var p = new Point
- {
- X = mouseEvent.MousePosition.X,
- Y = mouseEvent.MousePosition.Y
- };
-
- if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked)
- || (_lastMouseButtonPressed == null
- && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)
- && mouseEvent.ButtonState != 0
- && !_isButtonReleased
- && !_isButtonDoubleClicked))
- {
- switch (mouseEvent.ButtonState)
- {
- case WindowsConsole.ButtonState.Button1Pressed:
- mouseFlag = MouseFlags.Button1Pressed;
-
- break;
-
- case WindowsConsole.ButtonState.Button2Pressed:
- mouseFlag = MouseFlags.Button2Pressed;
-
- break;
-
- case WindowsConsole.ButtonState.RightmostButtonPressed:
- mouseFlag = MouseFlags.Button3Pressed;
-
- break;
- }
-
- if (_point is null)
- {
- _point = p;
- }
-
- if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved))
- {
- _pointMove = p;
- mouseFlag |= MouseFlags.ReportMousePosition;
- _isButtonReleased = false;
- _processButtonClick = false;
- }
-
- _lastMouseButtonPressed = mouseEvent.ButtonState;
- _isButtonPressed = true;
-
- if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
- {
- // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
- Application.MainLoop.AddIdle (
- () =>
- {
- Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
-
- return false;
- });
- }
- }
- else if (_lastMouseButtonPressed != null
- && mouseEvent.EventFlags == 0
- && !_isButtonReleased
- && !_isButtonDoubleClicked
- && !_isOneFingerDoubleClicked)
- {
- switch (_lastMouseButtonPressed)
- {
- case WindowsConsole.ButtonState.Button1Pressed:
- mouseFlag = MouseFlags.Button1Released;
-
- break;
-
- case WindowsConsole.ButtonState.Button2Pressed:
- mouseFlag = MouseFlags.Button2Released;
-
- break;
-
- case WindowsConsole.ButtonState.RightmostButtonPressed:
- mouseFlag = MouseFlags.Button3Released;
-
- break;
- }
-
- _isButtonPressed = false;
- _isButtonReleased = true;
-
- if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y)
- {
- _processButtonClick = true;
- }
- else
- {
- _point = null;
- }
- _processButtonClick = true;
-
- }
- else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
- && !_isOneFingerDoubleClicked
- && _isButtonReleased
- && p == _point)
- {
- mouseFlag = ProcessButtonClick (mouseEvent);
- }
- else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick))
- {
- switch (mouseEvent.ButtonState)
- {
- case WindowsConsole.ButtonState.Button1Pressed:
- mouseFlag = MouseFlags.Button1DoubleClicked;
-
- break;
-
- case WindowsConsole.ButtonState.Button2Pressed:
- mouseFlag = MouseFlags.Button2DoubleClicked;
-
- break;
-
- case WindowsConsole.ButtonState.RightmostButtonPressed:
- mouseFlag = MouseFlags.Button3DoubleClicked;
-
- break;
- }
-
- _isButtonDoubleClicked = true;
- }
- else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked)
- {
- switch (mouseEvent.ButtonState)
- {
- case WindowsConsole.ButtonState.Button1Pressed:
- mouseFlag = MouseFlags.Button1TripleClicked;
-
- break;
-
- case WindowsConsole.ButtonState.Button2Pressed:
- mouseFlag = MouseFlags.Button2TripleClicked;
-
- break;
-
- case WindowsConsole.ButtonState.RightmostButtonPressed:
- mouseFlag = MouseFlags.Button3TripleClicked;
-
- break;
- }
-
- _isButtonDoubleClicked = false;
- }
- else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled)
- {
- switch ((int)mouseEvent.ButtonState)
- {
- case int v when v > 0:
- mouseFlag = MouseFlags.WheeledUp;
-
- break;
-
- case int v when v < 0:
- mouseFlag = MouseFlags.WheeledDown;
-
- break;
- }
- }
- else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed)
- {
- switch ((int)mouseEvent.ButtonState)
- {
- case int v when v > 0:
- mouseFlag = MouseFlags.WheeledLeft;
-
- break;
-
- case int v when v < 0:
- mouseFlag = MouseFlags.WheeledRight;
-
- break;
- }
- }
- else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled)
- {
- switch ((int)mouseEvent.ButtonState)
- {
- case int v when v < 0:
- mouseFlag = MouseFlags.WheeledLeft;
-
- break;
-
- case int v when v > 0:
- mouseFlag = MouseFlags.WheeledRight;
-
- break;
- }
- }
- else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved)
- {
- mouseFlag = MouseFlags.ReportMousePosition;
-
- if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y)
- {
- _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
- }
- }
- else if (mouseEvent is { ButtonState: 0, EventFlags: 0 })
- {
- // This happens on a double or triple click event.
- mouseFlag = MouseFlags.None;
- }
-
- mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag);
-
- //System.Diagnostics.Debug.WriteLine (
- // $"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}");
-
- return new MouseEventArgs
- {
- Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y),
- Flags = mouseFlag
- };
- }
-}
-
-///
-/// Mainloop intended to be used with the , and can
-/// only be used on Windows.
-///
-///
-/// This implementation is used for WindowsDriver.
-///
-internal class WindowsMainLoop : IMainLoopDriver
-{
- ///
- /// Invoked when the window is changed.
- ///
- public EventHandler WinChanged;
-
- private readonly ConsoleDriver _consoleDriver;
- private readonly ManualResetEventSlim _eventReady = new (false);
-
- // The records that we keep fetching
- private readonly Queue _resultQueue = new ();
- private readonly ManualResetEventSlim _waitForProbe = new (false);
- private readonly WindowsConsole _winConsole;
- private CancellationTokenSource _eventReadyTokenSource = new ();
- private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
- private MainLoop _mainLoop;
-
- public WindowsMainLoop (ConsoleDriver consoleDriver = null)
- {
- _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
- _winConsole = ((WindowsDriver)consoleDriver).WinConsole;
- }
-
- void IMainLoopDriver.Setup (MainLoop mainLoop)
- {
- _mainLoop = mainLoop;
- Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
-#if HACK_CHECK_WINCHANGED
- Task.Run (CheckWinChange);
-#endif
- }
-
- void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
-
- bool IMainLoopDriver.EventsPending ()
- {
- _waitForProbe.Set ();
-#if HACK_CHECK_WINCHANGED
- _winChange.Set ();
-#endif
- 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);
- //
- }
- _eventReady.Reset ();
- }
- catch (OperationCanceledException)
- {
- _eventReady.Reset ();
- return true;
- }
- finally
- {
- //_eventReady.Reset ();
- }
-
- if (!_eventReadyTokenSource.IsCancellationRequested)
- {
-#if HACK_CHECK_WINCHANGED
- return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
-#else
- return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
-#endif
- }
-
- _eventReadyTokenSource.Dispose ();
- _eventReadyTokenSource = new CancellationTokenSource ();
-
- return true;
- }
-
- void IMainLoopDriver.Iteration ()
- {
- while (_resultQueue.Count > 0)
- {
- WindowsConsole.InputRecord [] inputRecords = _resultQueue.Dequeue ();
-
- if (inputRecords is { Length: > 0 })
- {
- ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]);
- }
- }
-#if HACK_CHECK_WINCHANGED
- if (_winChanged)
- {
- _winChanged = false;
- WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
- }
-#endif
- }
-
- void IMainLoopDriver.TearDown ()
- {
- _inputHandlerTokenSource?.Cancel ();
- _inputHandlerTokenSource?.Dispose ();
-
- if (_winConsole is { })
- {
- var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
-
- if (numOfEvents > 0)
- {
- _winConsole.FlushConsoleInputBuffer ();
- //Debug.WriteLine ($"Flushed {numOfEvents} events.");
- }
- }
-
- _waitForProbe?.Dispose ();
-
- _resultQueue?.Clear ();
-
- _eventReadyTokenSource?.Cancel ();
- _eventReadyTokenSource?.Dispose ();
- _eventReady?.Dispose ();
-
-#if HACK_CHECK_WINCHANGED
- _winChange?.Dispose ();
-#endif
-
- _mainLoop = null;
- }
-
- private void WindowsInputHandler ()
- {
- while (_mainLoop is { })
- {
- try
- {
- if (!_inputHandlerTokenSource.IsCancellationRequested)
- {
- _waitForProbe.Wait (_inputHandlerTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- // Wakes the _waitForProbe if it's waiting
- _waitForProbe.Set ();
- return;
- }
- finally
- {
- // If IsCancellationRequested is true the code after
- // the `finally` block will not be executed.
- if (!_inputHandlerTokenSource.IsCancellationRequested)
- {
- _waitForProbe.Reset ();
- }
- }
-
- if (_resultQueue?.Count == 0)
- {
- var input = _winConsole.ReadConsoleInput ();
-
- //if (input [0].EventType != WindowsConsole.EventType.Focus)
- {
- _resultQueue.Enqueue (input);
- }
- }
-
- if (_resultQueue?.Count > 0)
- {
- _eventReady.Set ();
- }
- }
- }
-
-#if HACK_CHECK_WINCHANGED
- private readonly ManualResetEventSlim _winChange = new (false);
- private bool _winChanged;
- private Size _windowSize;
- private void CheckWinChange ()
- {
- while (_mainLoop is { })
- {
- _winChange.Wait ();
- _winChange.Reset ();
-
- // Check if the window size changed every half second.
- // We do this to minimize the weird tearing seen on Windows when resizing the console
- while (_mainLoop is { })
- {
- Task.Delay (500).Wait ();
- _windowSize = _winConsole.GetConsoleBufferWindow (out _);
-
- if (_windowSize != Size.Empty
- && (_windowSize.Width != _consoleDriver.Cols
- || _windowSize.Height != _consoleDriver.Rows))
- {
- break;
- }
- }
-
- _winChanged = true;
- _eventReady.Set ();
- }
- }
-#endif
-}
-
-internal class WindowsClipboard : ClipboardBase
-{
- private const uint CF_UNICODE_TEXT = 13;
-
- public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
-
- private static bool CheckClipboardIsAvailable ()
- {
- // Attempt to open the clipboard
- if (OpenClipboard (nint.Zero))
- {
- // Clipboard is available
- // Close the clipboard after use
- CloseClipboard ();
-
- return true;
- }
- // Clipboard is not available
- return false;
- }
-
- protected override string GetClipboardDataImpl ()
- {
- try
- {
- if (!OpenClipboard (nint.Zero))
- {
- return string.Empty;
- }
-
- nint handle = GetClipboardData (CF_UNICODE_TEXT);
-
- if (handle == nint.Zero)
- {
- return string.Empty;
- }
-
- nint pointer = nint.Zero;
-
- try
- {
- pointer = GlobalLock (handle);
-
- if (pointer == nint.Zero)
- {
- return string.Empty;
- }
-
- int size = GlobalSize (handle);
- var buff = new byte [size];
-
- Marshal.Copy (pointer, buff, 0, size);
-
- return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
- }
- finally
- {
- if (pointer != nint.Zero)
- {
- GlobalUnlock (handle);
- }
- }
- }
- finally
- {
- CloseClipboard ();
- }
- }
-
- protected override void SetClipboardDataImpl (string text)
- {
- OpenClipboard ();
-
- EmptyClipboard ();
- nint hGlobal = default;
-
- try
- {
- int bytes = (text.Length + 1) * 2;
- hGlobal = Marshal.AllocHGlobal (bytes);
-
- if (hGlobal == default (nint))
- {
- ThrowWin32 ();
- }
-
- nint target = GlobalLock (hGlobal);
-
- if (target == default (nint))
- {
- ThrowWin32 ();
- }
-
- try
- {
- Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
- }
- finally
- {
- GlobalUnlock (target);
- }
-
- if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint))
- {
- ThrowWin32 ();
- }
-
- hGlobal = default (nint);
- }
- finally
- {
- if (hGlobal != default (nint))
- {
- Marshal.FreeHGlobal (hGlobal);
- }
-
- CloseClipboard ();
- }
- }
-
- [DllImport ("user32.dll", SetLastError = true)]
- [return: MarshalAs (UnmanagedType.Bool)]
- private static extern bool CloseClipboard ();
-
- [DllImport ("user32.dll")]
- private static extern bool EmptyClipboard ();
-
- [DllImport ("user32.dll", SetLastError = true)]
- private static extern nint GetClipboardData (uint uFormat);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern nint GlobalLock (nint hMem);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- private static extern int GlobalSize (nint handle);
-
- [DllImport ("kernel32.dll", SetLastError = true)]
- [return: MarshalAs (UnmanagedType.Bool)]
- private static extern bool GlobalUnlock (nint hMem);
-
- [DllImport ("User32.dll", SetLastError = true)]
- [return: MarshalAs (UnmanagedType.Bool)]
- private static extern bool IsClipboardFormatAvailable (uint format);
-
- private void OpenClipboard ()
- {
- var num = 10;
-
- while (true)
- {
- if (OpenClipboard (default (nint)))
- {
- break;
- }
-
- if (--num == 0)
- {
- ThrowWin32 ();
- }
-
- Thread.Sleep (100);
- }
- }
-
- [DllImport ("user32.dll", SetLastError = true)]
- [return: MarshalAs (UnmanagedType.Bool)]
- private static extern bool OpenClipboard (nint hWndNewOwner);
-
- [DllImport ("user32.dll", SetLastError = true)]
- private static extern nint SetClipboardData (uint uFormat, nint data);
-
- private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); }
-}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs
new file mode 100644
index 0000000000..d38ffb4082
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs
@@ -0,0 +1,179 @@
+#nullable enable
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui;
+
+internal class WindowsClipboard : ClipboardBase
+{
+ private const uint CF_UNICODE_TEXT = 13;
+
+ public override bool IsSupported { get; } = CheckClipboardIsAvailable ();
+
+ private static bool CheckClipboardIsAvailable ()
+ {
+ // Attempt to open the clipboard
+ if (OpenClipboard (nint.Zero))
+ {
+ // Clipboard is available
+ // Close the clipboard after use
+ CloseClipboard ();
+
+ return true;
+ }
+ // Clipboard is not available
+ return false;
+ }
+
+ protected override string GetClipboardDataImpl ()
+ {
+ try
+ {
+ if (!OpenClipboard (nint.Zero))
+ {
+ return string.Empty;
+ }
+
+ nint handle = GetClipboardData (CF_UNICODE_TEXT);
+
+ if (handle == nint.Zero)
+ {
+ return string.Empty;
+ }
+
+ nint pointer = nint.Zero;
+
+ try
+ {
+ pointer = GlobalLock (handle);
+
+ if (pointer == nint.Zero)
+ {
+ return string.Empty;
+ }
+
+ int size = GlobalSize (handle);
+ var buff = new byte [size];
+
+ Marshal.Copy (pointer, buff, 0, size);
+
+ return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
+ }
+ finally
+ {
+ if (pointer != nint.Zero)
+ {
+ GlobalUnlock (handle);
+ }
+ }
+ }
+ finally
+ {
+ CloseClipboard ();
+ }
+ }
+
+ protected override void SetClipboardDataImpl (string text)
+ {
+ OpenClipboard ();
+
+ EmptyClipboard ();
+ nint hGlobal = default;
+
+ try
+ {
+ int bytes = (text.Length + 1) * 2;
+ hGlobal = Marshal.AllocHGlobal (bytes);
+
+ if (hGlobal == default (nint))
+ {
+ ThrowWin32 ();
+ }
+
+ nint target = GlobalLock (hGlobal);
+
+ if (target == default (nint))
+ {
+ ThrowWin32 ();
+ }
+
+ try
+ {
+ Marshal.Copy (text.ToCharArray (), 0, target, text.Length);
+ }
+ finally
+ {
+ GlobalUnlock (target);
+ }
+
+ if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint))
+ {
+ ThrowWin32 ();
+ }
+
+ hGlobal = default (nint);
+ }
+ finally
+ {
+ if (hGlobal != default (nint))
+ {
+ Marshal.FreeHGlobal (hGlobal);
+ }
+
+ CloseClipboard ();
+ }
+ }
+
+ [DllImport ("user32.dll", SetLastError = true)]
+ [return: MarshalAs (UnmanagedType.Bool)]
+ private static extern bool CloseClipboard ();
+
+ [DllImport ("user32.dll")]
+ private static extern bool EmptyClipboard ();
+
+ [DllImport ("user32.dll", SetLastError = true)]
+ private static extern nint GetClipboardData (uint uFormat);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern nint GlobalLock (nint hMem);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern int GlobalSize (nint handle);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs (UnmanagedType.Bool)]
+ private static extern bool GlobalUnlock (nint hMem);
+
+ [DllImport ("User32.dll", SetLastError = true)]
+ [return: MarshalAs (UnmanagedType.Bool)]
+ private static extern bool IsClipboardFormatAvailable (uint format);
+
+ private void OpenClipboard ()
+ {
+ var num = 10;
+
+ while (true)
+ {
+ if (OpenClipboard (default (nint)))
+ {
+ break;
+ }
+
+ if (--num == 0)
+ {
+ ThrowWin32 ();
+ }
+
+ Thread.Sleep (100);
+ }
+ }
+
+ [DllImport ("user32.dll", SetLastError = true)]
+ [return: MarshalAs (UnmanagedType.Bool)]
+ private static extern bool OpenClipboard (nint hWndNewOwner);
+
+ [DllImport ("user32.dll", SetLastError = true)]
+ private static extern nint SetClipboardData (uint uFormat, nint data);
+
+ private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
new file mode 100644
index 0000000000..69d0de755e
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
@@ -0,0 +1,1209 @@
+#nullable enable
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using Terminal.Gui.ConsoleDrivers;
+
+namespace Terminal.Gui;
+
+internal class WindowsConsole
+{
+ private CancellationTokenSource? _inputReadyCancellationTokenSource;
+ private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ());
+ internal WindowsMainLoop? _mainLoop;
+
+ public const int STD_OUTPUT_HANDLE = -11;
+ public const int STD_INPUT_HANDLE = -10;
+
+ private readonly nint _inputHandle;
+ private nint _outputHandle;
+ //private nint _screenBuffer;
+ private readonly uint _originalConsoleMode;
+ private CursorVisibility? _initialCursorVisibility;
+ private CursorVisibility? _currentCursorVisibility;
+ private CursorVisibility? _pendingCursorVisibility;
+ private readonly StringBuilder _stringBuilder = new (256 * 1024);
+ private string _lastWrite = string.Empty;
+
+ public WindowsConsole ()
+ {
+ _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
+ _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
+ _originalConsoleMode = ConsoleMode;
+ uint newConsoleMode = _originalConsoleMode;
+ newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags);
+ newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode;
+ newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
+ ConsoleMode = newConsoleMode;
+
+ _inputReadyCancellationTokenSource = new ();
+ Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+ }
+
+ public InputRecord? DequeueInput ()
+ {
+ while (_inputReadyCancellationTokenSource is { })
+ {
+ try
+ {
+ return _inputQueue.Take (_inputReadyCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ public InputRecord? ReadConsoleInput ()
+ {
+ const int BUFFER_SIZE = 1;
+ InputRecord inputRecord = default;
+ uint numberEventsRead = 0;
+ StringBuilder ansiSequence = new StringBuilder ();
+ bool readingSequence = false;
+ bool raisedResponse = false;
+
+ while (!_inputReadyCancellationTokenSource!.IsCancellationRequested)
+ {
+ try
+ {
+ // Peek to check if there is any input available
+ if (PeekConsoleInput (_inputHandle, out _, BUFFER_SIZE, out uint eventsRead) && eventsRead > 0)
+ {
+ // Read the input since it is available
+ ReadConsoleInput (
+ _inputHandle,
+ out inputRecord,
+ BUFFER_SIZE,
+ out numberEventsRead);
+
+ if (inputRecord.EventType == EventType.Key)
+ {
+ KeyEventRecord keyEvent = inputRecord.KeyEvent;
+
+ if (keyEvent.bKeyDown)
+ {
+ char inputChar = keyEvent.UnicodeChar;
+
+ // Check if input is part of an ANSI escape sequence
+ if (inputChar == '\u001B') // Escape character
+ {
+ // Peek to check if there is any input available with key event and bKeyDown and not Escape character
+ if (PeekConsoleInput (_inputHandle, out InputRecord peekRecord, BUFFER_SIZE, out eventsRead) && eventsRead > 0)
+ {
+ if (peekRecord is { EventType: EventType.Key, KeyEvent.bKeyDown: true, KeyEvent.UnicodeChar: not '\u001B' })
+ {
+ // It's really an ANSI request response
+ readingSequence = true;
+ // Start a new sequence ensuring in the cases where wasn't clear by reading the terminator
+ ansiSequence.Clear ();
+ ansiSequence.Append (inputChar);
+
+ continue;
+ }
+ }
+ }
+ else if (readingSequence)
+ {
+ ansiSequence.Append (inputChar);
+
+ // Check if the sequence has ended with an expected command terminator
+ if (AnsiEscapeSequenceRequests.HasResponse (inputChar.ToString (), out AnsiEscapeSequenceRequestStatus? seqReqStatus))
+ {
+ // Finished reading the sequence and remove the enqueued request
+ AnsiEscapeSequenceRequests.Remove (seqReqStatus);
+
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ readingSequence = false;
+ raisedResponse = true;
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (ansiSequence.ToString (), seqReqStatus.AnsiRequest);
+ ClearInputRecord ();
+
+ // Clear the ansiSequence to avoid insert another Esc character
+ ansiSequence.Clear ();
+ }
+ }
+
+ if (readingSequence)
+ {
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ if (readingSequence && !raisedResponse && AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && AnsiEscapeSequenceRequests.Statuses.Count > 0)
+ {
+ AnsiEscapeSequenceRequests.Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus? seqReqStatus);
+
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (ansiSequence.ToString (), seqReqStatus.AnsiRequest);
+ ClearInputRecord();
+ }
+
+ _retries = 0;
+ }
+ else if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && AnsiEscapeSequenceRequests.Statuses.Count > 0)
+ {
+ if (_retries > 1)
+ {
+ if (AnsiEscapeSequenceRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.AnsiEscapeSequenceResponse?.Response))
+ {
+ lock (seqReqStatus.AnsiRequest._responseLock)
+ {
+ AnsiEscapeSequenceRequests.Statuses.TryDequeue (out _);
+
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (null, seqReqStatus.AnsiRequest);
+ ClearInputRecord();
+ }
+ }
+
+ _retries = 0;
+ }
+ else
+ {
+ _retries++;
+ }
+ }
+ else
+ {
+ _retries = 0;
+ }
+
+ if (numberEventsRead > 0)
+ {
+ return inputRecord;
+ }
+
+ if (!_forceRead)
+ {
+ try
+ {
+ Task.Delay (100, _inputReadyCancellationTokenSource.Token).Wait (_inputReadyCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ if (ex is OperationCanceledException or ObjectDisposedException)
+ {
+ return null;
+ }
+
+ throw;
+ }
+ }
+
+ return null;
+
+ void ClearInputRecord ()
+ {
+ // Clear the terminator for not be enqueued
+ inputRecord = default (InputRecord);
+
+ // Clear numberEventsRead to not exit
+ numberEventsRead = 0;
+ }
+ }
+
+ internal bool _forceRead;
+
+ private void ProcessInputQueue ()
+ {
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ try
+ {
+ if (_inputQueue.Count == 0 || _forceRead)
+ {
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ try
+ {
+ InputRecord? inpRec = ReadConsoleInput ();
+
+ if (inpRec is { })
+ {
+ _inputQueue.Add (inpRec.Value);
+
+ break;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ }
+ }
+
+
+ private CharInfo []? _originalStdOutChars;
+
+ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
+ {
+ //Debug.WriteLine ("WriteToConsole");
+
+ //if (_screenBuffer == nint.Zero)
+ //{
+ // ReadFromConsoleOutput (size, bufferSize, ref window);
+ //}
+
+ var result = false;
+
+ if (force16Colors)
+ {
+ var i = 0;
+ CharInfo [] ci = new CharInfo [charInfoBuffer.Length];
+
+ foreach (ExtendedCharInfo info in charInfoBuffer)
+ {
+ ci [i++] = new CharInfo
+ {
+ Char = new CharUnion { UnicodeChar = info.Char },
+ Attributes =
+ (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4))
+ };
+ }
+
+ result = WriteConsoleOutput (_outputHandle, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window);
+ }
+ else
+ {
+ _stringBuilder.Clear ();
+
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_SaveCursorPosition);
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (0, 0));
+
+ Attribute? prev = null;
+
+ foreach (ExtendedCharInfo info in charInfoBuffer)
+ {
+ Attribute attr = info.Attribute;
+
+ if (attr != prev)
+ {
+ prev = attr;
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
+ }
+
+ if (info.Char != '\x1b')
+ {
+ if (!info.Empty)
+ {
+ _stringBuilder.Append (info.Char);
+ }
+ }
+ else
+ {
+ _stringBuilder.Append (' ');
+ }
+ }
+
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_RestoreCursorPosition);
+ _stringBuilder.Append (AnsiEscapeSequenceRequestUtils.CSI_HideCursor);
+
+ var s = _stringBuilder.ToString ();
+
+ // TODO: requires extensive testing if we go down this route
+ // If console output has changed
+ if (s != _lastWrite)
+ {
+ // supply console with the new content
+ result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero);
+ }
+
+ _lastWrite = s;
+
+ foreach (var sixel in Application.Sixel)
+ {
+ SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
+ WriteConsole (_outputHandle, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
+ }
+ }
+
+ if (!result)
+ {
+ int err = Marshal.GetLastWin32Error ();
+
+ if (err != 0)
+ {
+ throw new Win32Exception (err);
+ }
+ }
+
+ return result;
+ }
+
+ internal bool WriteANSI (string ansi)
+ {
+ if (WriteConsole (_outputHandle, ansi, (uint)ansi.Length, out uint _, nint.Zero))
+ {
+ // Flush the output to make sure it's sent immediately
+ return FlushFileBuffers (_outputHandle);
+ }
+
+ return false;
+ }
+
+ public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window)
+ {
+ //_screenBuffer = CreateConsoleScreenBuffer (
+ // DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
+ // ShareMode.FileShareRead | ShareMode.FileShareWrite,
+ // nint.Zero,
+ // 1,
+ // nint.Zero
+ // );
+
+ //if (_screenBuffer == INVALID_HANDLE_VALUE)
+ //{
+ // int err = Marshal.GetLastWin32Error ();
+
+ // if (err != 0)
+ // {
+ // throw new Win32Exception (err);
+ // }
+ //}
+
+ SetInitialCursorVisibility ();
+
+ //if (!SetConsoleActiveScreenBuffer (_screenBuffer))
+ //{
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ //}
+
+ _originalStdOutChars = new CharInfo [size.Height * size.Width];
+
+ if (!ReadConsoleOutput (_outputHandle, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window))
+ {
+ throw new Win32Exception (Marshal.GetLastWin32Error ());
+ }
+ }
+
+ public bool SetCursorPosition (Coord position)
+ {
+ return SetConsoleCursorPosition (_outputHandle, position);
+ }
+
+ public void SetInitialCursorVisibility ()
+ {
+ if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility))
+ {
+ _initialCursorVisibility = visibility;
+ }
+ }
+
+ public bool GetCursorVisibility (out CursorVisibility visibility)
+ {
+ if (_outputHandle == nint.Zero)
+ {
+ visibility = CursorVisibility.Invisible;
+
+ return false;
+ }
+
+ if (!GetConsoleCursorInfo (_outputHandle, out ConsoleCursorInfo info))
+ {
+ int err = Marshal.GetLastWin32Error ();
+
+ if (err != 0)
+ {
+ throw new Win32Exception (err);
+ }
+
+ visibility = CursorVisibility.Default;
+
+ return false;
+ }
+
+ if (!info.bVisible)
+ {
+ visibility = CursorVisibility.Invisible;
+ }
+ else if (info.dwSize > 50)
+ {
+ visibility = CursorVisibility.Default;
+ }
+ else
+ {
+ visibility = CursorVisibility.Default;
+ }
+
+ return true;
+ }
+
+ public bool EnsureCursorVisibility ()
+ {
+ if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value))
+ {
+ _pendingCursorVisibility = null;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void ForceRefreshCursorVisibility ()
+ {
+ if (_currentCursorVisibility.HasValue)
+ {
+ _pendingCursorVisibility = _currentCursorVisibility;
+ _currentCursorVisibility = null;
+ }
+ }
+
+ public bool SetCursorVisibility (CursorVisibility visibility)
+ {
+ if (_initialCursorVisibility.HasValue == false)
+ {
+ _pendingCursorVisibility = visibility;
+
+ return false;
+ }
+
+ if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility)
+ {
+ var info = new ConsoleCursorInfo
+ {
+ dwSize = (uint)visibility & 0x00FF,
+ bVisible = ((uint)visibility & 0xFF00) != 0
+ };
+
+ if (!SetConsoleCursorInfo (_outputHandle, ref info))
+ {
+ return false;
+ }
+
+ _currentCursorVisibility = visibility;
+ }
+
+ return true;
+ }
+
+ public void Cleanup ()
+ {
+ if (_initialCursorVisibility.HasValue)
+ {
+ SetCursorVisibility (_initialCursorVisibility.Value);
+ }
+
+ //SetConsoleOutputWindow (out _);
+
+ ConsoleMode = _originalConsoleMode;
+
+ _outputHandle = CreateConsoleScreenBuffer (
+ DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
+ ShareMode.FileShareRead | ShareMode.FileShareWrite,
+ nint.Zero,
+ 1,
+ nint.Zero
+ );
+
+ if (!SetConsoleActiveScreenBuffer (_outputHandle))
+ {
+ int err = Marshal.GetLastWin32Error ();
+ Console.WriteLine ("Error: {0}", err);
+ }
+
+ //if (_screenBuffer != nint.Zero)
+ //{
+ // CloseHandle (_screenBuffer);
+ //}
+
+ //_screenBuffer = nint.Zero;
+
+ _inputReadyCancellationTokenSource?.Cancel ();
+ _inputReadyCancellationTokenSource?.Dispose ();
+ _inputReadyCancellationTokenSource = null;
+ }
+
+ internal Size GetConsoleBufferWindow (out Point position)
+ {
+ if (_outputHandle == nint.Zero)
+ {
+ position = Point.Empty;
+
+ return Size.Empty;
+ }
+
+ var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+ csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+ if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+ {
+ //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
+ position = Point.Empty;
+
+ return Size.Empty;
+ }
+
+ Size sz = new (
+ csbi.srWindow.Right - csbi.srWindow.Left + 1,
+ csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+ position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+
+ return sz;
+ }
+
+ internal Size GetConsoleOutputWindow (out Point position)
+ {
+ var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+ csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+ if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+ {
+ throw new Win32Exception (Marshal.GetLastWin32Error ());
+ }
+
+ Size sz = new (
+ csbi.srWindow.Right - csbi.srWindow.Left + 1,
+ csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+ position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+
+ return sz;
+ }
+
+ //internal Size SetConsoleWindow (short cols, short rows)
+ //{
+ // var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+ // csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+ // if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+
+ // Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer);
+ // short newCols = Math.Min (cols, maxWinSize.X);
+ // short newRows = Math.Min (rows, maxWinSize.Y);
+ // csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1));
+ // csbi.srWindow = new SmallRect (0, 0, newCols, newRows);
+ // csbi.dwMaximumWindowSize = new Coord (newCols, newRows);
+
+ // if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+
+ // var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
+
+ // if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
+ // {
+ // //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
+ // return new (cols, rows);
+ // }
+
+ // SetConsoleOutputWindow (csbi);
+
+ // return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
+ //}
+
+ //private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi)
+ //{
+ // if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+ //}
+
+ //internal Size SetConsoleOutputWindow (out Point position)
+ //{
+ // if (_screenBuffer == nint.Zero)
+ // {
+ // position = Point.Empty;
+
+ // return Size.Empty;
+ // }
+
+ // var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX ();
+ // csbi.cbSize = (uint)Marshal.SizeOf (csbi);
+
+ // if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+
+ // Size sz = new (
+ // csbi.srWindow.Right - csbi.srWindow.Left + 1,
+ // Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0));
+ // position = new (csbi.srWindow.Left, csbi.srWindow.Top);
+ // SetConsoleOutputWindow (csbi);
+ // var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
+
+ // if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+
+ // if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
+ // {
+ // throw new Win32Exception (Marshal.GetLastWin32Error ());
+ // }
+
+ // return sz;
+ //}
+
+ private uint ConsoleMode
+ {
+ get
+ {
+ GetConsoleMode (_inputHandle, out uint v);
+
+ return v;
+ }
+ set => SetConsoleMode (_inputHandle, value);
+ }
+
+ [Flags]
+ public enum ConsoleModes : uint
+ {
+ EnableProcessedInput = 1,
+ EnableMouseInput = 16,
+ EnableQuickEditMode = 64,
+ EnableExtendedFlags = 128
+ }
+
+ [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+ public struct KeyEventRecord
+ {
+ [FieldOffset (0)]
+ [MarshalAs (UnmanagedType.Bool)]
+ public bool bKeyDown;
+
+ [FieldOffset (4)]
+ [MarshalAs (UnmanagedType.U2)]
+ public ushort wRepeatCount;
+
+ [FieldOffset (6)]
+ [MarshalAs (UnmanagedType.U2)]
+ public ConsoleKeyMapping.VK wVirtualKeyCode;
+
+ [FieldOffset (8)]
+ [MarshalAs (UnmanagedType.U2)]
+ public ushort wVirtualScanCode;
+
+ [FieldOffset (10)]
+ public char UnicodeChar;
+
+ [FieldOffset (12)]
+ [MarshalAs (UnmanagedType.U4)]
+ public ControlKeyState dwControlKeyState;
+
+ public readonly override string ToString ()
+ {
+ return
+ $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]";
+ }
+ }
+
+ [Flags]
+ public enum ButtonState
+ {
+ NoButtonPressed = 0,
+ Button1Pressed = 1,
+ Button2Pressed = 4,
+ Button3Pressed = 8,
+ Button4Pressed = 16,
+ RightmostButtonPressed = 2
+ }
+
+ [Flags]
+ public enum ControlKeyState
+ {
+ NoControlKeyPressed = 0,
+ RightAltPressed = 1,
+ LeftAltPressed = 2,
+ RightControlPressed = 4,
+ LeftControlPressed = 8,
+ ShiftPressed = 16,
+ NumlockOn = 32,
+ ScrolllockOn = 64,
+ CapslockOn = 128,
+ EnhancedKey = 256
+ }
+
+ [Flags]
+ public enum EventFlags
+ {
+ NoEvent = 0,
+ MouseMoved = 1,
+ DoubleClick = 2,
+ MouseWheeled = 4,
+ MouseHorizontalWheeled = 8
+ }
+
+ [StructLayout (LayoutKind.Explicit)]
+ public struct MouseEventRecord
+ {
+ [FieldOffset (0)]
+ public Coord MousePosition;
+
+ [FieldOffset (4)]
+ public ButtonState ButtonState;
+
+ [FieldOffset (8)]
+ public ControlKeyState ControlKeyState;
+
+ [FieldOffset (12)]
+ public EventFlags EventFlags;
+
+ public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; }
+ }
+
+ public struct WindowBufferSizeRecord
+ {
+ public Coord _size;
+
+ public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); }
+
+ public readonly override string ToString () { return $"[WindowBufferSize{_size}"; }
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct MenuEventRecord
+ {
+ public uint dwCommandId;
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct FocusEventRecord
+ {
+ public uint bSetFocus;
+ }
+
+ public enum EventType : ushort
+ {
+ Focus = 0x10,
+ Key = 0x1,
+ Menu = 0x8,
+ Mouse = 2,
+ WindowBufferSize = 4
+ }
+
+ [StructLayout (LayoutKind.Explicit)]
+ public struct InputRecord
+ {
+ [FieldOffset (0)]
+ public EventType EventType;
+
+ [FieldOffset (4)]
+ public KeyEventRecord KeyEvent;
+
+ [FieldOffset (4)]
+ public MouseEventRecord MouseEvent;
+
+ [FieldOffset (4)]
+ public WindowBufferSizeRecord WindowBufferSizeEvent;
+
+ [FieldOffset (4)]
+ public MenuEventRecord MenuEvent;
+
+ [FieldOffset (4)]
+ public FocusEventRecord FocusEvent;
+
+ public readonly override string ToString ()
+ {
+ return (EventType switch
+ {
+ EventType.Focus => FocusEvent.ToString (),
+ EventType.Key => KeyEvent.ToString (),
+ EventType.Menu => MenuEvent.ToString (),
+ EventType.Mouse => MouseEvent.ToString (),
+ EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
+ _ => "Unknown event type: " + EventType
+ })!;
+ }
+ }
+
+ [Flags]
+ private enum ShareMode : uint
+ {
+ FileShareRead = 1,
+ FileShareWrite = 2
+ }
+
+ [Flags]
+ private enum DesiredAccess : uint
+ {
+ GenericRead = 2147483648,
+ GenericWrite = 1073741824
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct ConsoleScreenBufferInfo
+ {
+ public Coord dwSize;
+ public Coord dwCursorPosition;
+ public ushort wAttributes;
+ public SmallRect srWindow;
+ public Coord dwMaximumWindowSize;
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct Coord
+ {
+ public short X;
+ public short Y;
+
+ public Coord (short x, short y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ public readonly override string ToString () { return $"({X},{Y})"; }
+ }
+
+ [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+ public struct CharUnion
+ {
+ [FieldOffset (0)]
+ public char UnicodeChar;
+
+ [FieldOffset (0)]
+ public byte AsciiChar;
+ }
+
+ [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)]
+ public struct CharInfo
+ {
+ [FieldOffset (0)]
+ public CharUnion Char;
+
+ [FieldOffset (2)]
+ public ushort Attributes;
+ }
+
+ public struct ExtendedCharInfo
+ {
+ public char Char { get; set; }
+ public Attribute Attribute { get; set; }
+ public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences
+
+ public ExtendedCharInfo (char character, Attribute attribute)
+ {
+ Char = character;
+ Attribute = attribute;
+ Empty = false;
+ }
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct SmallRect
+ {
+ public short Left;
+ public short Top;
+ public short Right;
+ public short Bottom;
+
+ public SmallRect (short left, short top, short right, short bottom)
+ {
+ Left = left;
+ Top = top;
+ Right = right;
+ Bottom = bottom;
+ }
+
+ public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; }
+
+ public static void Update (ref SmallRect rect, short col, short row)
+ {
+ if (rect.Left == -1)
+ {
+ rect.Left = rect.Right = col;
+ rect.Bottom = rect.Top = row;
+
+ return;
+ }
+
+ if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom)
+ {
+ return;
+ }
+
+ if (col < rect.Left)
+ {
+ rect.Left = col;
+ }
+
+ if (col > rect.Right)
+ {
+ rect.Right = col;
+ }
+
+ if (row < rect.Top)
+ {
+ rect.Top = row;
+ }
+
+ if (row > rect.Bottom)
+ {
+ rect.Bottom = row;
+ }
+ }
+
+ public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; }
+ }
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct ConsoleKeyInfoEx
+ {
+ public ConsoleKeyInfo ConsoleKeyInfo;
+ public bool CapsLock;
+ public bool NumLock;
+ public bool ScrollLock;
+
+ public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock)
+ {
+ ConsoleKeyInfo = consoleKeyInfo;
+ CapsLock = capslock;
+ NumLock = numlock;
+ ScrollLock = scrolllock;
+ }
+
+ ///
+ /// Prints a ConsoleKeyInfoEx structure
+ ///
+ ///
+ ///
+ public readonly string ToString (ConsoleKeyInfoEx ex)
+ {
+ var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar);
+ var sb = new StringBuilder ();
+ sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})");
+ sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
+ sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
+ sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
+ sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) ");
+ sb.Append (ex.CapsLock ? "caps," : string.Empty);
+ sb.Append (ex.NumLock ? "num," : string.Empty);
+ sb.Append (ex.ScrollLock ? "scroll," : string.Empty);
+ string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
+
+ return $"[ConsoleKeyInfoEx({s})]";
+ }
+ }
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern nint GetStdHandle (int nStdHandle);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool CloseHandle (nint handle);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ public static extern bool PeekConsoleInput (nint hConsoleInput, out InputRecord lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
+
+ [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
+ public static extern bool ReadConsoleInput (
+ nint hConsoleInput,
+ out InputRecord lpBuffer,
+ uint nLength,
+ out uint lpNumberOfEventsRead
+ );
+
+ [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool ReadConsoleOutput (
+ nint hConsoleOutput,
+ [Out] CharInfo [] lpBuffer,
+ Coord dwBufferSize,
+ Coord dwBufferCoord,
+ ref SmallRect lpReadRegion
+ );
+
+ // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput
+ [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool WriteConsoleOutput (
+ nint hConsoleOutput,
+ CharInfo [] lpBuffer,
+ Coord dwBufferSize,
+ Coord dwBufferCoord,
+ ref SmallRect lpWriteRegion
+ );
+
+ [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool WriteConsole (
+ nint hConsoleOutput,
+ string lpbufer,
+ uint NumberOfCharsToWriten,
+ out uint lpNumberOfCharsWritten,
+ nint lpReserved
+ );
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ static extern bool FlushFileBuffers (nint hFile);
+
+ [DllImport ("kernel32.dll")]
+ private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct ConsoleCursorInfo
+ {
+ ///
+ /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100.
+ /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal
+ /// line at the bottom of the cell.
+ ///
+ public uint dwSize;
+ public bool bVisible;
+ }
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo);
+
+ [DllImport ("kernel32.dll")]
+ private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
+
+ [DllImport ("kernel32.dll")]
+ private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern nint CreateConsoleScreenBuffer (
+ DesiredAccess dwDesiredAccess,
+ ShareMode dwShareMode,
+ nint secutiryAttributes,
+ uint flags,
+ nint screenBufferData
+ );
+
+ internal static nint INVALID_HANDLE_VALUE = new (-1);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleActiveScreenBuffer (nint handle);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents);
+
+ internal uint GetNumberOfConsoleInputEvents ()
+ {
+ if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents))
+ {
+ Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
+
+ return 0;
+ }
+
+ return numOfEvents;
+ }
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool FlushConsoleInputBuffer (nint handle);
+
+ internal void FlushConsoleInputBuffer ()
+ {
+ if (!FlushConsoleInputBuffer (_inputHandle))
+ {
+ Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}");
+ }
+ }
+
+ private int _retries;
+
+#if false // Not needed on the constructor. Perhaps could be used on resizing. To study.
+ [DllImport ("kernel32.dll", ExactSpelling = true)]
+ static extern IntPtr GetConsoleWindow ();
+
+ [DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ static extern bool ShowWindow (IntPtr hWnd, int nCmdShow);
+
+ public const int HIDE = 0;
+ public const int MAXIMIZE = 3;
+ public const int MINIMIZE = 6;
+ public const int RESTORE = 9;
+
+ internal void ShowWindow (int state)
+ {
+ IntPtr thisConsole = GetConsoleWindow ();
+ ShowWindow (thisConsole, state);
+ }
+#endif
+
+ // See: https://github.com/gui-cs/Terminal.Gui/issues/357
+
+ [StructLayout (LayoutKind.Sequential)]
+ public struct CONSOLE_SCREEN_BUFFER_INFOEX
+ {
+ public uint cbSize;
+ public Coord dwSize;
+ public Coord dwCursorPosition;
+ public ushort wAttributes;
+ public SmallRect srWindow;
+ public Coord dwMaximumWindowSize;
+ public ushort wPopupAttributes;
+ public bool bFullscreenSupported;
+
+ [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)]
+ public COLORREF [] ColorTable;
+ }
+
+ [StructLayout (LayoutKind.Explicit, Size = 4)]
+ public struct COLORREF
+ {
+ public COLORREF (byte r, byte g, byte b)
+ {
+ Value = 0;
+ R = r;
+ G = g;
+ B = b;
+ }
+
+ public COLORREF (uint value)
+ {
+ R = 0;
+ G = 0;
+ B = 0;
+ Value = value & 0x00FFFFFF;
+ }
+
+ [FieldOffset (0)]
+ public byte R;
+
+ [FieldOffset (1)]
+ public byte G;
+
+ [FieldOffset (2)]
+ public byte B;
+
+ [FieldOffset (0)]
+ public uint Value;
+ }
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo);
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern bool SetConsoleWindowInfo (
+ nint hConsoleOutput,
+ bool bAbsolute,
+ [In] ref SmallRect lpConsoleWindow
+ );
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern Coord GetLargestConsoleWindowSize (
+ nint hConsoleOutput
+ );
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
new file mode 100644
index 0000000000..51df2c4d8b
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
@@ -0,0 +1,1192 @@
+#nullable enable
+//
+// WindowsDriver.cs: Windows specific driver
+//
+
+// HACK:
+// WindowsConsole/Terminal has two issues:
+// 1) Tearing can occur when the console is resized.
+// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct.
+//
+// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events
+// and instead check the console size every 500ms in a thread in WidowsMainLoop.
+// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using
+// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is
+// still incorrect so we still need this hack.
+
+//#define HACK_CHECK_WINCHANGED
+
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
+
+namespace Terminal.Gui;
+
+internal class WindowsDriver : ConsoleDriver
+{
+ private readonly bool _isWindowsTerminal;
+
+ private WindowsConsole.SmallRect _damageRegion;
+ private bool _isButtonDoubleClicked;
+ private bool _isButtonPressed;
+ private bool _isButtonReleased;
+ private bool _isOneFingerDoubleClicked;
+
+ private WindowsConsole.ButtonState? _lastMouseButtonPressed;
+ private WindowsMainLoop? _mainLoopDriver;
+ private WindowsConsole.ExtendedCharInfo [] _outputBuffer;
+ private Point? _point;
+ private Point _pointMove;
+ private bool _processButtonClick;
+
+ public WindowsDriver ()
+ {
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ {
+ WinConsole = new ();
+
+ // otherwise we're probably running in unit tests
+ Clipboard = new WindowsClipboard ();
+ }
+ else
+ {
+ Clipboard = new FakeDriver.FakeClipboard ();
+ }
+
+ // TODO: if some other Windows-based terminal supports true color, update this logic to not
+ // force 16color mode (.e.g ConEmu which really doesn't work well at all).
+ _isWindowsTerminal = _isWindowsTerminal =
+ Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null;
+
+ if (!_isWindowsTerminal)
+ {
+ Force16Colors = true;
+ }
+ }
+
+ public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal);
+
+ public WindowsConsole? WinConsole { get; private set; }
+
+ public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent)
+ {
+ if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet)
+ {
+ return keyEvent;
+ }
+
+ var mod = new ConsoleModifiers ();
+
+ if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
+ {
+ mod |= ConsoleModifiers.Shift;
+ }
+
+ if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
+ || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
+ {
+ mod |= ConsoleModifiers.Alt;
+ }
+
+ if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)
+ || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed))
+ {
+ mod |= ConsoleModifiers.Control;
+ }
+
+ var cKeyInfo = new ConsoleKeyInfo (
+ keyEvent.UnicodeChar,
+ (ConsoleKey)keyEvent.wVirtualKeyCode,
+ mod.HasFlag (ConsoleModifiers.Shift),
+ mod.HasFlag (ConsoleModifiers.Alt),
+ mod.HasFlag (ConsoleModifiers.Control));
+ cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
+ uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo);
+
+ return new WindowsConsole.KeyEventRecord
+ {
+ UnicodeChar = cKeyInfo.KeyChar,
+ bKeyDown = keyEvent.bKeyDown,
+ dwControlKeyState = keyEvent.dwControlKeyState,
+ wRepeatCount = keyEvent.wRepeatCount,
+ wVirtualKeyCode = (VK)cKeyInfo.Key,
+ wVirtualScanCode = (ushort)scanCode
+ };
+ }
+
+ public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; }
+
+ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+ {
+ var input = new WindowsConsole.InputRecord
+ {
+ EventType = WindowsConsole.EventType.Key
+ };
+
+ var keyEvent = new WindowsConsole.KeyEventRecord
+ {
+ bKeyDown = true
+ };
+ var controlKey = new WindowsConsole.ControlKeyState ();
+
+ if (shift)
+ {
+ controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
+ keyEvent.UnicodeChar = '\0';
+ keyEvent.wVirtualKeyCode = VK.SHIFT;
+ }
+
+ if (alt)
+ {
+ controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
+ controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
+ keyEvent.UnicodeChar = '\0';
+ keyEvent.wVirtualKeyCode = VK.MENU;
+ }
+
+ if (control)
+ {
+ controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
+ controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
+ keyEvent.UnicodeChar = '\0';
+ keyEvent.wVirtualKeyCode = VK.CONTROL;
+ }
+
+ keyEvent.dwControlKeyState = controlKey;
+
+ input.KeyEvent = keyEvent;
+
+ if (shift || alt || control)
+ {
+ ProcessInput (input);
+ }
+
+ keyEvent.UnicodeChar = keyChar;
+
+ //if ((uint)key < 255) {
+ // keyEvent.wVirtualKeyCode = (ushort)key;
+ //} else {
+ // keyEvent.wVirtualKeyCode = '\0';
+ //}
+ keyEvent.wVirtualKeyCode = (VK)key;
+
+ input.KeyEvent = keyEvent;
+
+ try
+ {
+ ProcessInput (input);
+ }
+ catch (OverflowException)
+ { }
+ finally
+ {
+ keyEvent.bKeyDown = false;
+ input.KeyEvent = keyEvent;
+ ProcessInput (input);
+ }
+ }
+
+ internal override void WriteRaw (string ansi) { WinConsole?.WriteANSI (ansi); }
+
+ #region Not Implemented
+
+ public override void Suspend () { throw new NotImplementedException (); }
+
+ #endregion
+
+ public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
+ {
+ WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
+
+ bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0;
+ bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0;
+ bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0;
+ bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0;
+ bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0;
+ bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0;
+
+ var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control);
+
+ return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock);
+ }
+
+ #region Cursor Handling
+
+ private CursorVisibility? _cachedCursorVisibility;
+
+ public override void UpdateCursor ()
+ {
+ if (RunningUnitTests)
+ {
+ return;
+ }
+
+ if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows)
+ {
+ GetCursorVisibility (out CursorVisibility cursorVisibility);
+ _cachedCursorVisibility = cursorVisibility;
+ SetCursorVisibility (CursorVisibility.Invisible);
+
+ return;
+ }
+
+ var position = new WindowsConsole.Coord
+ {
+ X = (short)Col,
+ Y = (short)Row
+ };
+
+ if (Force16Colors)
+ {
+ WinConsole?.SetCursorPosition (position);
+ }
+ else
+ {
+ var sb = new StringBuilder ();
+ sb.Append (AnsiEscapeSequenceRequestUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
+ WinConsole?.WriteANSI (sb.ToString ());
+ }
+
+ if (_cachedCursorVisibility is { })
+ {
+ SetCursorVisibility (_cachedCursorVisibility.Value);
+ }
+ //EnsureCursorVisibility ();
+ }
+
+ ///
+ public override bool GetCursorVisibility (out CursorVisibility visibility)
+ {
+ if (WinConsole is { })
+ {
+ return WinConsole.GetCursorVisibility (out visibility);
+ }
+
+ visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
+
+ return true;
+ }
+
+ ///
+ public override bool SetCursorVisibility (CursorVisibility visibility)
+ {
+ _cachedCursorVisibility = visibility;
+
+ if (Force16Colors)
+ {
+ return WinConsole is null || WinConsole.SetCursorVisibility (visibility);
+ }
+ else
+ {
+ var sb = new StringBuilder ();
+ sb.Append (visibility != CursorVisibility.Invisible ? AnsiEscapeSequenceRequestUtils.CSI_ShowCursor : AnsiEscapeSequenceRequestUtils.CSI_HideCursor);
+ return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
+ }
+ }
+
+ ///
+ public override bool EnsureCursorVisibility ()
+ {
+ if (Force16Colors)
+ {
+ return WinConsole is null || WinConsole.EnsureCursorVisibility ();
+ }
+ else
+ {
+ var sb = new StringBuilder ();
+ sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? AnsiEscapeSequenceRequestUtils.CSI_ShowCursor : AnsiEscapeSequenceRequestUtils.CSI_HideCursor);
+ return WinConsole?.WriteANSI (sb.ToString ()) ?? false;
+ }
+
+ //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 Cursor Handling
+
+ public override bool UpdateScreen ()
+ {
+ bool updated = false;
+ Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows);
+
+ if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows))
+ {
+ return updated;
+ }
+
+ var bufferCoords = new WindowsConsole.Coord
+ {
+ X = (short)Cols, //Clip.Width,
+ Y = (short)Rows, //Clip.Height
+ };
+
+ for (var row = 0; row < Rows; row++)
+ {
+ if (!_dirtyLines! [row])
+ {
+ continue;
+ }
+
+ _dirtyLines [row] = false;
+ updated = true;
+
+ for (var col = 0; col < Cols; col++)
+ {
+ int position = row * Cols + col;
+ _outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault ();
+
+ if (Contents [row, col].IsDirty == false)
+ {
+ _outputBuffer [position].Empty = true;
+ _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+ continue;
+ }
+
+ _outputBuffer [position].Empty = false;
+
+ if (Contents [row, col].Rune.IsBmp)
+ {
+ _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value;
+ }
+ else
+ {
+ //_outputBuffer [position].Empty = true;
+ _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value;
+
+ if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols)
+ {
+ // TODO: This is a hack to deal with non-BMP and wide characters.
+ col++;
+ position = row * Cols + col;
+ _outputBuffer [position].Empty = false;
+ _outputBuffer [position].Char = ' ';
+ }
+ }
+ }
+ }
+
+ _damageRegion = new WindowsConsole.SmallRect
+ {
+ Top = 0,
+ Left = 0,
+ Bottom = (short)Rows,
+ Right = (short)Cols
+ };
+
+ if (!RunningUnitTests
+ && WinConsole != null
+ && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors))
+ {
+ int err = Marshal.GetLastWin32Error ();
+
+ if (err != 0)
+ {
+ throw new Win32Exception (err);
+ }
+ }
+
+ WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
+
+ return updated;
+ }
+
+ internal override void End ()
+ {
+ if (_mainLoopDriver is { })
+ {
+#if HACK_CHECK_WINCHANGED
+
+ _mainLoopDriver.WinChanged -= ChangeWin;
+#endif
+ }
+
+ _mainLoopDriver = null;
+
+ WinConsole?.Cleanup ();
+ WinConsole = null;
+
+ if (!RunningUnitTests && _isWindowsTerminal)
+ {
+ // Disable alternative screen buffer.
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+ }
+ }
+
+ internal override MainLoop Init ()
+ {
+ _mainLoopDriver = new WindowsMainLoop (this);
+
+ if (!RunningUnitTests)
+ {
+ try
+ {
+ if (WinConsole is { })
+ {
+ // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init.
+ // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED
+ Size winSize = WinConsole.GetConsoleOutputWindow (out Point _);
+ Cols = winSize.Width;
+ Rows = winSize.Height;
+ OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+ }
+
+ WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion);
+
+ if (_isWindowsTerminal)
+ {
+ Console.Out.Write (AnsiEscapeSequenceRequestUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+ }
+ }
+ catch (Win32Exception e)
+ {
+ // We are being run in an environment that does not support a console
+ // such as a unit test, or a pipe.
+ Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}");
+ WinConsole = null;
+ }
+ }
+
+ CurrentAttribute = new Attribute (Color.White, Color.Black);
+
+ _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (Screen);
+
+ _damageRegion = new WindowsConsole.SmallRect
+ {
+ Top = 0,
+ Left = 0,
+ Bottom = (short)Rows,
+ Right = (short)Cols
+ };
+
+ ClearContents ();
+
+#if HACK_CHECK_WINCHANGED
+ _mainLoopDriver.WinChanged = ChangeWin;
+#endif
+
+ if (!RunningUnitTests)
+ {
+ WinConsole?.SetInitialCursorVisibility ();
+
+ Task.Run (ProcessAnsiRequestHandler);
+ }
+
+ return new MainLoop (_mainLoopDriver);
+ }
+
+ internal void ProcessInput (WindowsConsole.InputRecord inputEvent)
+ {
+ switch (inputEvent.EventType)
+ {
+ case WindowsConsole.EventType.Key:
+ if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet)
+ {
+ // Used to pass Unicode characters as if they were keystrokes.
+ // The VK_PACKET key is the low word of a 32-bit
+ // Virtual Key value used for non-keyboard input methods.
+ inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent);
+ }
+
+ WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent);
+
+ //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}");
+
+ KeyCode map = MapKey (keyInfo);
+
+ if (map == KeyCode.Null)
+ {
+ break;
+ }
+
+ if (inputEvent.KeyEvent.bKeyDown)
+ {
+ // Avoid sending repeat key down events
+ OnKeyDown (new Key (map));
+ }
+ else
+ {
+ OnKeyUp (new Key (map));
+ }
+
+ break;
+
+ case WindowsConsole.EventType.Mouse:
+ MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
+
+ if (me.Flags == MouseFlags.None)
+ {
+ break;
+ }
+
+ OnMouseEvent (me);
+
+ if (_processButtonClick)
+ {
+ OnMouseEvent (new ()
+ {
+ Position = me.Position,
+ Flags = ProcessButtonClick (inputEvent.MouseEvent)
+ });
+ }
+
+ break;
+
+ case WindowsConsole.EventType.Focus:
+ break;
+
+#if !HACK_CHECK_WINCHANGED
+ case WindowsConsole.EventType.WindowBufferSize:
+
+ Cols = inputEvent.WindowBufferSizeEvent._size.X;
+ Rows = inputEvent.WindowBufferSizeEvent._size.Y;
+ Application.Screen = new (0, 0, Cols, Rows);
+
+ ResizeScreen ();
+ ClearContents ();
+ Application.Top?.SetNeedsLayout ();
+ Application.LayoutAndDraw ();
+
+ break;
+#endif
+ }
+ }
+
+#if HACK_CHECK_WINCHANGED
+ private void ChangeWin (object s, SizeChangedEventArgs e)
+ {
+ if (e.Size is null)
+ {
+ return;
+ }
+
+ int w = e.Size.Value.Width;
+
+ if (w == Cols - 3 && e.Size.Value.Height < Rows)
+ {
+ w += 3;
+ }
+
+ Left = 0;
+ Top = 0;
+ Cols = e.Size.Value.Width;
+ Rows = e.Size.Value.Height;
+
+ if (!RunningUnitTests)
+ {
+ Size newSize = WinConsole.SetConsoleWindow (
+ (short)Math.Max (w, 16),
+ (short)Math.Max (e.Size.Value.Height, 0));
+
+ Cols = newSize.Width;
+ Rows = newSize.Height;
+ }
+
+ ResizeScreen ();
+ ClearContents ();
+ OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
+ }
+#endif
+
+ private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
+ {
+ ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
+
+ switch (keyInfo.Key)
+ {
+ case ConsoleKey.D0:
+ case ConsoleKey.D1:
+ case ConsoleKey.D2:
+ case ConsoleKey.D3:
+ case ConsoleKey.D4:
+ case ConsoleKey.D5:
+ case ConsoleKey.D6:
+ case ConsoleKey.D7:
+ case ConsoleKey.D8:
+ case ConsoleKey.D9:
+ case ConsoleKey.NumPad0:
+ case ConsoleKey.NumPad1:
+ case ConsoleKey.NumPad2:
+ case ConsoleKey.NumPad3:
+ case ConsoleKey.NumPad4:
+ case ConsoleKey.NumPad5:
+ case ConsoleKey.NumPad6:
+ case ConsoleKey.NumPad7:
+ case ConsoleKey.NumPad8:
+ case ConsoleKey.NumPad9:
+ 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:
+ case ConsoleKey.Multiply:
+ case ConsoleKey.Add:
+ case ConsoleKey.Separator:
+ case ConsoleKey.Subtract:
+ case ConsoleKey.Decimal:
+ case ConsoleKey.Divide:
+ case ConsoleKey.OemPeriod:
+ case ConsoleKey.OemComma:
+ case ConsoleKey.OemPlus:
+ case ConsoleKey.OemMinus:
+ // These virtual key codes are mapped differently depending on the keyboard layout in use.
+ // We use the Win32 API to map them to the correct character.
+ uint mapResult = MapVKtoChar ((VK)keyInfo.Key);
+
+ if (mapResult == 0)
+ {
+ // There is no mapping - this should not happen
+ Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}.");
+
+ return KeyCode.Null;
+ }
+
+ // An un-shifted character value is in the low order word of the return value.
+ var mappedChar = (char)(mapResult & 0x0000FFFF);
+
+ if (keyInfo.KeyChar == 0)
+ {
+ // If the keyChar is 0, keyInfo.Key value is not a printable character.
+
+ // Dead keys (diacritics) are indicated by setting the top bit of the return value.
+ if ((mapResult & 0x80000000) != 0)
+ {
+ // Dead key (e.g. Oem2 '~'/'^' on POR keyboard)
+ // Option 1: Throw it out.
+ // - Apps will never see the dead keys
+ // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�').
+ // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a').
+ // - If user presses dead key again, the right thing happens (app will see `~~`)
+ // - This is what Notepad etc... appear to do
+ // Option 2: Expand the API to indicate the KeyCode is a dead key
+ // - Enables apps to do their own dead key processing
+ // - Adds complexity; no dev has asked for this (yet).
+ // We choose Option 1 for now.
+ return KeyCode.Null;
+
+ // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported.
+ // Sadly, the charVal is just the deadkey and subsequent key events do not contain
+ // any info that the previous event was a deadkey.
+ // Note WT does not support Ctrl-Deadkey either.
+ }
+
+ if (keyInfo.Modifiers != 0)
+ {
+ // These Oem keys have well-defined chars. We ensure the representative char is used.
+ // If we don't do this, then on some keyboard layouts the wrong char is
+ // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important
+ // for key persistence ("Ctrl++" vs. "Ctrl+=").
+ mappedChar = keyInfo.Key switch
+ {
+ ConsoleKey.OemPeriod => '.',
+ ConsoleKey.OemComma => ',',
+ ConsoleKey.OemPlus => '+',
+ ConsoleKey.OemMinus => '-',
+ _ => mappedChar
+ };
+ }
+
+ // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down
+ // we should keep it
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
+ }
+
+ // KeyChar is printable
+ if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+ {
+ // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar
+ return (KeyCode)keyInfo.KeyChar;
+ }
+
+ if (keyInfo.Modifiers != ConsoleModifiers.Shift)
+ {
+ // If Shift wasn't down we don't need to do anything but return the mappedChar
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar);
+ }
+
+ // 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);
+ }
+
+ // A..Z are special cased:
+ // - Alone, they represent lowercase a...z
+ // - With ShiftMask they are A..Z
+ // - If CapsLock is on the above is reversed.
+ // - If Alt and/or Ctrl are present, treat as upper case
+ if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
+ {
+ if (keyInfo.KeyChar == 0)
+ {
+ // KeyChar is not printable - possibly an AltGr key?
+ // AltGr support - AltGr is equivalent to Ctrl+Alt
+ if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+ }
+ }
+
+ if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+ }
+
+ if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock)
+ {
+ // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask
+ if (char.IsUpper (keyInfo.KeyChar))
+ {
+ if (keyInfo.KeyChar <= 'Z')
+ {
+ return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
+ }
+
+ // Always return the KeyChar because it may be an Á, À with Oem1, etc
+ return (KeyCode)keyInfo.KeyChar;
+ }
+ }
+
+ if (keyInfo.KeyChar <= 'z')
+ {
+ return (KeyCode)keyInfo.Key;
+ }
+
+ // Always return the KeyChar because it may be an á, à with Oem1, etc
+ return (KeyCode)keyInfo.KeyChar;
+ }
+
+ // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
+ if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
+ {
+ // If the key is JUST a modifier, return it as just that key
+ if (keyInfo.Key == (ConsoleKey)VK.SHIFT)
+ { // Shift 16
+ return KeyCode.ShiftMask;
+ }
+
+ if (keyInfo.Key == (ConsoleKey)VK.CONTROL)
+ { // Ctrl 17
+ return KeyCode.CtrlMask;
+ }
+
+ if (keyInfo.Key == (ConsoleKey)VK.MENU)
+ { // Alt 18
+ return KeyCode.AltMask;
+ }
+
+ if (keyInfo.KeyChar == 0)
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+ }
+
+ if (keyInfo.Key != ConsoleKey.None)
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+ }
+
+ return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
+ }
+
+ // Handle control keys (e.g. CursorUp)
+ if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
+ }
+
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+ }
+
+ private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent)
+ {
+ MouseFlags mouseFlag = 0;
+
+ switch (_lastMouseButtonPressed)
+ {
+ case WindowsConsole.ButtonState.Button1Pressed:
+ mouseFlag = MouseFlags.Button1Clicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.Button2Pressed:
+ mouseFlag = MouseFlags.Button2Clicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.RightmostButtonPressed:
+ mouseFlag = MouseFlags.Button3Clicked;
+
+ break;
+ }
+
+ _point = new Point
+ {
+ X = mouseEvent.MousePosition.X,
+ Y = mouseEvent.MousePosition.Y
+ };
+ _lastMouseButtonPressed = null;
+ _isButtonReleased = false;
+ _processButtonClick = false;
+ _point = null;
+
+ return mouseFlag;
+ }
+
+ private async Task ProcessButtonDoubleClickedAsync ()
+ {
+ await Task.Delay (200);
+ _isButtonDoubleClicked = false;
+ _isOneFingerDoubleClicked = false;
+
+ //buttonPressedCount = 0;
+ }
+
+ private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag)
+ {
+ // When a user presses-and-holds, start generating pressed events every `startDelay`
+ // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms
+ const int START_DELAY = 500;
+ const int ITERATIONS_UNTIL_FAST = 4;
+ const int FAST_DELAY = 50;
+
+ int iterations = 0;
+ int delay = START_DELAY;
+ while (_isButtonPressed)
+ {
+ // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+ View? view = Application.WantContinuousButtonPressedView;
+
+ if (view is null)
+ {
+ break;
+ }
+
+ if (iterations++ >= ITERATIONS_UNTIL_FAST)
+ {
+ delay = FAST_DELAY;
+ }
+ await Task.Delay (delay);
+
+ var me = new MouseEventArgs
+ {
+ ScreenPosition = _pointMove,
+ Flags = mouseFlag
+ };
+
+ //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}");
+ if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0)
+ {
+ // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+ Application.Invoke (() => OnMouseEvent (me));
+ }
+ }
+ }
+
+ private void ResizeScreen ()
+ {
+ _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols];
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (Screen);
+
+ _damageRegion = new WindowsConsole.SmallRect
+ {
+ Top = 0,
+ Left = 0,
+ Bottom = (short)Rows,
+ Right = (short)Cols
+ };
+ _dirtyLines = new bool [Rows];
+
+ WinConsole?.ForceRefreshCursorVisibility ();
+ }
+
+ private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag)
+ {
+ if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)
+ || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed))
+ {
+ mouseFlag |= MouseFlags.ButtonCtrl;
+ }
+
+ if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed))
+ {
+ mouseFlag |= MouseFlags.ButtonShift;
+ }
+
+ if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed)
+ || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed))
+ {
+ mouseFlag |= MouseFlags.ButtonAlt;
+ }
+
+ return mouseFlag;
+ }
+
+ [CanBeNull]
+ private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent)
+ {
+ var mouseFlag = MouseFlags.AllEvents;
+
+ //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}");
+
+ if (_isButtonDoubleClicked || _isOneFingerDoubleClicked)
+ {
+ // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+ Application.MainLoop!.AddIdle (
+ () =>
+ {
+ Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
+
+ return false;
+ });
+ }
+
+ // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button.
+ // This will tell when a mouse button is pressed. When the button is released this event will
+ // be fired with its bit set to 0. So when the button is up ButtonState will be 0.
+ // To map to the correct driver events we save the last pressed mouse button, so we can
+ // map to the correct clicked event.
+ if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0)
+ {
+ _lastMouseButtonPressed = null;
+
+ //isButtonPressed = false;
+ _isButtonReleased = false;
+ }
+
+ var p = new Point
+ {
+ X = mouseEvent.MousePosition.X,
+ Y = mouseEvent.MousePosition.Y
+ };
+
+ if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked)
+ || (_lastMouseButtonPressed == null
+ && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)
+ && mouseEvent.ButtonState != 0
+ && !_isButtonReleased
+ && !_isButtonDoubleClicked))
+ {
+ switch (mouseEvent.ButtonState)
+ {
+ case WindowsConsole.ButtonState.Button1Pressed:
+ mouseFlag = MouseFlags.Button1Pressed;
+
+ break;
+
+ case WindowsConsole.ButtonState.Button2Pressed:
+ mouseFlag = MouseFlags.Button2Pressed;
+
+ break;
+
+ case WindowsConsole.ButtonState.RightmostButtonPressed:
+ mouseFlag = MouseFlags.Button3Pressed;
+
+ break;
+ }
+
+ if (_point is null)
+ {
+ _point = p;
+ }
+
+ if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved))
+ {
+ _pointMove = p;
+ mouseFlag |= MouseFlags.ReportMousePosition;
+ _isButtonReleased = false;
+ _processButtonClick = false;
+ }
+
+ _lastMouseButtonPressed = mouseEvent.ButtonState;
+ _isButtonPressed = true;
+
+ if ((mouseFlag & MouseFlags.ReportMousePosition) == 0)
+ {
+ // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
+ Application.MainLoop!.AddIdle (
+ () =>
+ {
+ Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag));
+
+ return false;
+ });
+ }
+ }
+ else if (_lastMouseButtonPressed != null
+ && mouseEvent.EventFlags == 0
+ && !_isButtonReleased
+ && !_isButtonDoubleClicked
+ && !_isOneFingerDoubleClicked)
+ {
+ switch (_lastMouseButtonPressed)
+ {
+ case WindowsConsole.ButtonState.Button1Pressed:
+ mouseFlag = MouseFlags.Button1Released;
+
+ break;
+
+ case WindowsConsole.ButtonState.Button2Pressed:
+ mouseFlag = MouseFlags.Button2Released;
+
+ break;
+
+ case WindowsConsole.ButtonState.RightmostButtonPressed:
+ mouseFlag = MouseFlags.Button3Released;
+
+ break;
+ }
+
+ _isButtonPressed = false;
+ _isButtonReleased = true;
+
+ if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y)
+ {
+ _processButtonClick = true;
+ }
+ else
+ {
+ _point = null;
+ }
+ _processButtonClick = true;
+
+ }
+ else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved
+ && !_isOneFingerDoubleClicked
+ && _isButtonReleased
+ && p == _point)
+ {
+ mouseFlag = ProcessButtonClick (mouseEvent);
+ }
+ else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick))
+ {
+ switch (mouseEvent.ButtonState)
+ {
+ case WindowsConsole.ButtonState.Button1Pressed:
+ mouseFlag = MouseFlags.Button1DoubleClicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.Button2Pressed:
+ mouseFlag = MouseFlags.Button2DoubleClicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.RightmostButtonPressed:
+ mouseFlag = MouseFlags.Button3DoubleClicked;
+
+ break;
+ }
+
+ _isButtonDoubleClicked = true;
+ }
+ else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked)
+ {
+ switch (mouseEvent.ButtonState)
+ {
+ case WindowsConsole.ButtonState.Button1Pressed:
+ mouseFlag = MouseFlags.Button1TripleClicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.Button2Pressed:
+ mouseFlag = MouseFlags.Button2TripleClicked;
+
+ break;
+
+ case WindowsConsole.ButtonState.RightmostButtonPressed:
+ mouseFlag = MouseFlags.Button3TripleClicked;
+
+ break;
+ }
+
+ _isButtonDoubleClicked = false;
+ }
+ else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled)
+ {
+ switch ((int)mouseEvent.ButtonState)
+ {
+ case int v when v > 0:
+ mouseFlag = MouseFlags.WheeledUp;
+
+ break;
+
+ case int v when v < 0:
+ mouseFlag = MouseFlags.WheeledDown;
+
+ break;
+ }
+ }
+ else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed)
+ {
+ switch ((int)mouseEvent.ButtonState)
+ {
+ case int v when v > 0:
+ mouseFlag = MouseFlags.WheeledLeft;
+
+ break;
+
+ case int v when v < 0:
+ mouseFlag = MouseFlags.WheeledRight;
+
+ break;
+ }
+ }
+ else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled)
+ {
+ switch ((int)mouseEvent.ButtonState)
+ {
+ case int v when v < 0:
+ mouseFlag = MouseFlags.WheeledLeft;
+
+ break;
+
+ case int v when v > 0:
+ mouseFlag = MouseFlags.WheeledRight;
+
+ break;
+ }
+ }
+ else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved)
+ {
+ mouseFlag = MouseFlags.ReportMousePosition;
+
+ if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y)
+ {
+ _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y);
+ }
+ }
+ else if (mouseEvent is { ButtonState: 0, EventFlags: 0 })
+ {
+ // This happens on a double or triple click event.
+ mouseFlag = MouseFlags.None;
+ }
+
+ mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag);
+
+ //System.Diagnostics.Debug.WriteLine (
+ // $"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}");
+
+ return new MouseEventArgs
+ {
+ Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y),
+ Flags = mouseFlag
+ };
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
new file mode 100644
index 0000000000..13c8421fde
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs
@@ -0,0 +1,235 @@
+#nullable enable
+
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+///
+/// Mainloop intended to be used with the , and can
+/// only be used on Windows.
+///
+///
+/// This implementation is used for WindowsDriver.
+///
+internal class WindowsMainLoop : IMainLoopDriver
+{
+ ///
+ /// Invoked when the window is changed.
+ ///
+ public EventHandler? WinChanged;
+
+ private readonly ConsoleDriver _consoleDriver;
+ private readonly ManualResetEventSlim _eventReady = new (false);
+
+ // The records that we keep fetching
+ private readonly ConcurrentQueue _resultQueue = new ();
+ ManualResetEventSlim IMainLoopDriver.WaitForInput { get; set; } = new (false);
+ private readonly WindowsConsole? _winConsole;
+ private CancellationTokenSource _eventReadyTokenSource = new ();
+ private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
+ private MainLoop? _mainLoop;
+
+ public WindowsMainLoop (ConsoleDriver consoleDriver)
+ {
+ _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+
+ if (!ConsoleDriver.RunningUnitTests)
+ {
+ _winConsole = ((WindowsDriver)consoleDriver).WinConsole;
+ _winConsole!._mainLoop = this;
+ }
+ }
+
+ void IMainLoopDriver.Setup (MainLoop mainLoop)
+ {
+ _mainLoop = mainLoop;
+
+ if (ConsoleDriver.RunningUnitTests)
+ {
+ return;
+ }
+
+ Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token);
+#if HACK_CHECK_WINCHANGED
+ Task.Run (CheckWinChange);
+#endif
+ }
+
+ void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+ bool IMainLoopDriver.EventsPending ()
+ {
+ ((IMainLoopDriver)this).WaitForInput.Set ();
+#if HACK_CHECK_WINCHANGED
+ _winChange.Set ();
+#endif
+ if (_resultQueue.Count > 0 || _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 ();
+ }
+
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+#if HACK_CHECK_WINCHANGED
+ return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
+#else
+ return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+#endif
+ }
+
+ _eventReadyTokenSource.Dispose ();
+ _eventReadyTokenSource = new CancellationTokenSource ();
+
+ // If cancellation was requested then always return true
+ return true;
+ }
+
+ void IMainLoopDriver.Iteration ()
+ {
+ while (_resultQueue.TryDequeue (out WindowsConsole.InputRecord inputRecords))
+ {
+ ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords);
+ }
+#if HACK_CHECK_WINCHANGED
+ if (_winChanged)
+ {
+ _winChanged = false;
+ WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize));
+ }
+#endif
+ }
+
+ void IMainLoopDriver.TearDown ()
+ {
+ _inputHandlerTokenSource.Cancel ();
+ _inputHandlerTokenSource.Dispose ();
+
+ if (_winConsole is { })
+ {
+ var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents ();
+
+ if (numOfEvents > 0)
+ {
+ _winConsole.FlushConsoleInputBuffer ();
+ //Debug.WriteLine ($"Flushed {numOfEvents} events.");
+ }
+ }
+
+ ((IMainLoopDriver)this).WaitForInput?.Dispose ();
+
+ _resultQueue.Clear ();
+
+ _eventReadyTokenSource.Cancel ();
+ _eventReadyTokenSource.Dispose ();
+ _eventReady.Dispose ();
+
+#if HACK_CHECK_WINCHANGED
+ _winChange?.Dispose ();
+#endif
+
+ _mainLoop = null;
+ }
+
+ public bool ForceRead { get; set; }
+
+ private void WindowsInputHandler ()
+ {
+ while (_mainLoop is { })
+ {
+ try
+ {
+ if (_inputHandlerTokenSource.IsCancellationRequested && !ForceRead)
+ {
+ try
+ {
+ ((IMainLoopDriver)this).WaitForInput.Wait (_inputHandlerTokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ if (ex is OperationCanceledException or ObjectDisposedException)
+ {
+ return;
+ }
+
+ throw;
+ }
+
+ ((IMainLoopDriver)this).WaitForInput.Reset ();
+ }
+
+ ProcessInputQueue ();
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ }
+ }
+
+ private void ProcessInputQueue ()
+ {
+ if (_resultQueue?.Count == 0 || ForceRead)
+ {
+ WindowsConsole.InputRecord? result = _winConsole!.DequeueInput ();
+
+ if (result.HasValue)
+ {
+ _resultQueue!.Enqueue (result.Value);
+
+ _eventReady.Set ();
+ }
+ }
+ }
+
+#if HACK_CHECK_WINCHANGED
+ private readonly ManualResetEventSlim _winChange = new (false);
+ private bool _winChanged;
+ private Size _windowSize;
+ private void CheckWinChange ()
+ {
+ while (_mainLoop is { })
+ {
+ _winChange.Wait ();
+ _winChange.Reset ();
+
+ // Check if the window size changed every half second.
+ // We do this to minimize the weird tearing seen on Windows when resizing the console
+ while (_mainLoop is { })
+ {
+ Task.Delay (500).Wait ();
+ _windowSize = _winConsole.GetConsoleBufferWindow (out _);
+
+ if (_windowSize != Size.Empty
+ && (_windowSize.Width != _consoleDriver.Cols
+ || _windowSize.Height != _consoleDriver.Rows))
+ {
+ break;
+ }
+ }
+
+ _winChanged = true;
+ _eventReady.Set ();
+ }
+ }
+#endif
+}
+
diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Key.cs
index 11f8d938e2..f5cd3e8bc2 100644
--- a/Terminal.Gui/Input/Key.cs
+++ b/Terminal.Gui/Input/Key.cs
@@ -706,7 +706,7 @@ out parsedInt
if (GetIsKeyCodeAtoZ (keyCode) && (keyCode & KeyCode.Space) != 0)
{
- keyCode = keyCode & ~KeyCode.Space;
+ keyCode &= ~KeyCode.Space;
}
key = new (keyCode | modifiers);
diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj
index 21bfa3b9ea..ea071cfb08 100644
--- a/Terminal.Gui/Terminal.Gui.csproj
+++ b/Terminal.Gui/Terminal.Gui.csproj
@@ -11,9 +11,9 @@
-
- Terminal.Gui
-
+
+ Terminal.Gui
+
@@ -154,22 +154,39 @@
$(MSBuildThisFileDirectory)bin\$(Configuration)\
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
+
+
+
+
+
+ true
+ compiled-binaries/
+ PreserveNewest
+
+
+
+
+ true
+ compiled-binaries/
+ PreserveNewest
+
+
+
\ No newline at end of file
diff --git a/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.dylib b/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.dylib
new file mode 100644
index 0000000000..06c30292e9
Binary files /dev/null and b/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.dylib differ
diff --git a/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.so b/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.so
new file mode 100644
index 0000000000..d3fd5e00be
Binary files /dev/null and b/Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.so differ
diff --git a/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
new file mode 100644
index 0000000000..80ffdb6ea3
--- /dev/null
+++ b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
@@ -0,0 +1,440 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("AnsiEscapeSequenceRequest", "Ansi Escape Sequence Request")]
+[ScenarioCategory ("Tests")]
+public sealed class AnsiEscapeSequenceRequests : Scenario
+{
+ private readonly Dictionary _sends = new ();
+
+ private readonly object _lockAnswers = new ();
+ private readonly Dictionary _answers = new ();
+ private readonly Dictionary _errors = new ();
+
+ private GraphView _graphView;
+
+ private ScatterSeries _sentSeries;
+ private ScatterSeries _answeredSeries;
+ private Label _lblSummary;
+ private Label _lblErrorSummary;
+
+ public override void Main ()
+ {
+ // Init
+ Application.Init ();
+
+ var tv = new TabView
+ {
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ var single = new Tab ();
+ single.DisplayText = "_Single";
+ single.View = BuildSingleTab ();
+
+ Tab bulk = new ();
+ bulk.DisplayText = "_Multi";
+ bulk.View = BuildBulkTab ();
+
+ tv.AddTab (single, true);
+ tv.AddTab (bulk, false);
+
+ // Setup - Create a top-level application window and configure it.
+ Window appWindow = new ()
+ {
+ Title = GetQuitKeyAndName ()
+ };
+
+ appWindow.Add (tv);
+
+ // Run - Start the application.
+ Application.Run (appWindow);
+ bulk.View.Dispose ();
+ single.View.Dispose ();
+ appWindow.Dispose ();
+
+ // Shutdown - Calling Application.Shutdown is required.
+ Application.Shutdown ();
+ }
+
+ private View BuildBulkTab ()
+ {
+ var w = new View
+ {
+ Width = Dim.Fill (),
+ Height = Dim.Fill (),
+ CanFocus = true
+ };
+
+ 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.",
+ Height = 2,
+ Width = Dim.Fill ()
+ };
+
+ Application.AddTimeout (
+ TimeSpan.FromMilliseconds (1000),
+ () =>
+ {
+ lock (_lockAnswers)
+ {
+ UpdateGraph ();
+
+ UpdateResponses ();
+ }
+
+ return true;
+ });
+
+ var tv = new TextView
+ {
+ Y = Pos.Bottom (lbl),
+ Width = Dim.Percent (50),
+ Height = Dim.Fill ()
+ };
+
+ var lblDar = new Label
+ {
+ Y = Pos.Bottom (lbl),
+ X = Pos.Right (tv) + 1,
+ Text = "_DAR per second: "
+ };
+
+ var cbDar = new NumericUpDown
+ {
+ X = Pos.Right (lblDar),
+ Y = Pos.Bottom (lbl),
+ Value = 0
+ };
+
+ cbDar.ValueChanging += (s, e) =>
+ {
+ if (e.NewValue is < 0 or > 20)
+ {
+ e.Cancel = true;
+ }
+ };
+ w.Add (cbDar);
+
+ int lastSendTime = Environment.TickCount;
+ var lockObj = new object ();
+ int interval = 50;
+
+ Application.AddTimeout (
+ TimeSpan.FromMilliseconds ((double)interval / (cbDar.Value > 0 ? cbDar.Value : 1)),
+ () =>
+ {
+ lock (lockObj)
+ {
+ if (cbDar.Value > 0)
+ {
+ interval = 1000 / cbDar.Value; // Calculate the desired interval in milliseconds
+ int currentTime = Environment.TickCount; // Current system time in milliseconds
+
+ // Check if the time elapsed since the last send is greater than the interval
+ if (currentTime - lastSendTime >= interval)
+ {
+ SendDar (); // Send the request
+ lastSendTime = currentTime; // Update the last send time
+ }
+ }
+ }
+
+ return true;
+ });
+
+ _graphView = new ()
+ {
+ Y = Pos.Bottom (cbDar),
+ X = Pos.Right (tv),
+ Width = Dim.Fill (),
+ Height = Dim.Fill (2)
+ };
+
+ _lblSummary = new ()
+ {
+ Y = Pos.Bottom (_graphView),
+ X = Pos.Right (tv),
+ Width = Dim.Fill ()
+ };
+
+ _lblErrorSummary = new ()
+ {
+ Y = Pos.Bottom (_lblSummary),
+ X = Pos.Right (tv),
+ Width = Dim.Fill ()
+ };
+
+ SetupGraph ();
+
+ w.Add (lbl);
+ w.Add (lblDar);
+ w.Add (cbDar);
+ w.Add (tv);
+ w.Add (_graphView);
+ w.Add (_lblSummary);
+ w.Add (_lblErrorSummary);
+
+ return w;
+ }
+
+ private View BuildSingleTab ()
+ {
+ var w = new View
+ {
+ Width = Dim.Fill (),
+ Height = Dim.Fill (),
+ CanFocus = true
+ };
+
+ w.Padding.Thickness = new (1);
+
+ // TODO: This hackery is why I think the EscSeqUtils class should be refactored and the CSI's made type safe.
+ List scrRequests = new ()
+ {
+ "CSI_SendDeviceAttributes",
+ "CSI_ReportTerminalSizeInChars",
+ "CSI_RequestCursorPositionReport",
+ "CSI_SendDeviceAttributes2"
+ };
+
+ var cbRequests = new ComboBox { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) };
+ w.Add (cbRequests);
+
+ // 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 () { 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 () { X = Pos.Left (tfValue) + label.Text.Length, 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);
+
+ cbRequests.SelectedItemChanged += (s, e) =>
+ {
+ if (cbRequests.SelectedItem == -1)
+ {
+ return;
+ }
+
+ string selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
+ AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null;
+
+ switch (selAnsiEscapeSequenceRequestName)
+ {
+ case "CSI_SendDeviceAttributes":
+ selAnsiEscapeSequenceRequest = AnsiEscapeSequenceRequestUtils.CSI_SendDeviceAttributes;
+
+ break;
+ case "CSI_ReportTerminalSizeInChars":
+ selAnsiEscapeSequenceRequest = AnsiEscapeSequenceRequestUtils.CSI_ReportTerminalSizeInChars;
+
+ break;
+ case "CSI_RequestCursorPositionReport":
+ selAnsiEscapeSequenceRequest = AnsiEscapeSequenceRequestUtils.CSI_RequestCursorPositionReport;
+
+ break;
+ case "CSI_SendDeviceAttributes2":
+ selAnsiEscapeSequenceRequest = AnsiEscapeSequenceRequestUtils.CSI_SendDeviceAttributes2;
+
+ break;
+ }
+
+ tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : "";
+
+ tfValue.Text = selAnsiEscapeSequenceRequest is { }
+ ? selAnsiEscapeSequenceRequest.ExpectedResponseValue ?? ""
+ : "";
+ tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : "";
+ };
+
+ // Forces raise cbRequests.SelectedItemChanged to update TextFields
+ cbRequests.SelectedItem = 0;
+
+ 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 () { 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 () { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "_Value:" };
+ var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true };
+ w.Add (label, tvValue);
+
+ 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 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,
+ ExpectedResponseValue = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
+ };
+
+ bool success = Application.Driver!.TryWriteAnsiRequest (
+ Application.MainLoop!.MainLoopDriver,
+ ref ansiEscapeSequenceRequest
+ );
+
+ tvResponse.Text = ansiEscapeSequenceRequest.AnsiEscapeSequenceResponse?.Response ?? "";
+ tvError.Text = ansiEscapeSequenceRequest.AnsiEscapeSequenceResponse?.Error ?? "";
+ tvValue.Text = ansiEscapeSequenceRequest.AnsiEscapeSequenceResponse?.ExpectedResponseValue ?? "";
+ tvTerminator.Text = ansiEscapeSequenceRequest.AnsiEscapeSequenceResponse?.Terminator ?? "";
+
+ 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 = "Send other requests by editing the TextFields." });
+
+ return w;
+ }
+
+ private string GetSummary ()
+ {
+ if (_answers.Count == 0)
+ {
+ return "No requests sent yet";
+ }
+
+ string last = _answers.Last ().Value.AnsiEscapeSequenceResponse!.Response;
+
+ int unique = _answers.Values.Distinct ().Count ();
+ int total = _answers.Count;
+
+ return $"Last:{last} U:{unique} T:{total}";
+ }
+
+ private string GetSummaryErrors ()
+ {
+ if (_errors.Count == 0)
+ {
+ return "No errors received yet";
+ }
+
+ string last = _errors.Last ().Value.AnsiEscapeSequenceResponse!.Error;
+
+ int unique = _errors.Values.Distinct ().Count ();
+ int total = _errors.Count;
+
+ return $"Last:{last} U:{unique} T:{total}";
+ }
+
+ private void HandleResponse (AnsiEscapeSequenceRequest ansiRequest)
+ {
+ lock (_lockAnswers)
+ {
+ _answers.Add (DateTime.Now, ansiRequest);
+ KeyValuePair found = _sends.First (r => r.Value == ansiRequest);
+ _sends.Remove (found.Key);
+ }
+ }
+
+ private void HandleResponseError (AnsiEscapeSequenceRequest ansiRequest)
+ {
+ lock (_lockAnswers)
+ {
+ _errors.Add (DateTime.Now, ansiRequest);
+ KeyValuePair found = _sends.First (r => r.Value == ansiRequest);
+ _sends.Remove (found.Key);
+ }
+ }
+
+ private void SendDar ()
+ {
+ AnsiEscapeSequenceRequest ansiRequest = AnsiEscapeSequenceRequestUtils.CSI_SendDeviceAttributes;
+ _sends.Add (DateTime.Now, ansiRequest);
+
+ if (Application.Driver!.TryWriteAnsiRequest (Application.MainLoop!.MainLoopDriver, ref ansiRequest))
+ {
+ HandleResponse (ansiRequest);
+ }
+ else
+ {
+ HandleResponseError (ansiRequest);
+ }
+ }
+
+ private void SetupGraph ()
+ {
+ _graphView.Series.Add (_sentSeries = new ());
+ _graphView.Series.Add (_answeredSeries = new ());
+
+ _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 (1, 1);
+ _graphView.MarginBottom = 2;
+ _graphView.AxisX.Increment = 1;
+ _graphView.AxisX.Text = "Seconds";
+ _graphView.GraphColor = new Attribute (Color.Green, Color.Black);
+ }
+
+ private static Func, int> ToSeconds () { return t => (int)(DateTime.Now - t.Key).TotalSeconds; }
+
+ private void UpdateGraph ()
+ {
+ System.Diagnostics.Debug.Assert (_sends.Count == 0);
+
+ _sentSeries.Points = _sends
+ .GroupBy (ToSeconds ())
+ .Select (g => new PointF (g.Key, g.Count ()))
+ .ToList ();
+
+ _answeredSeries.Points = _answers
+ .Where (
+ r => r.Value.AnsiEscapeSequenceResponse is { }
+ && !string.IsNullOrEmpty (r.Value?.AnsiEscapeSequenceResponse.Response))
+ .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.SetNeedsDraw ();
+ }
+ }
+
+ private void UpdateResponses ()
+ {
+ _lblSummary.Text = GetSummary ();
+ _lblSummary.SetNeedsDraw ();
+
+ _lblErrorSummary.Text = GetSummaryErrors ();
+ _lblErrorSummary.SetNeedsDraw ();
+ }
+}
diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs
index c0289893ee..1bd06b30c9 100644
--- a/UnitTests/Application/ApplicationTests.cs
+++ b/UnitTests/Application/ApplicationTests.cs
@@ -579,6 +579,27 @@ void Application_Iteration (object sender, IterationEventArgs e)
}
}
+ [Fact]
+ public void Screen_Size_Changes ()
+ {
+ var driver = new FakeDriver ();
+ Application.Init (driver);
+ Assert.Equal (new (0, 0, 80, 25), driver.Screen);
+ Assert.Equal (new (0, 0, 80, 25), Application.Screen);
+
+ driver.Cols = 100;
+ driver.Rows = 30;
+ // ConsoleDriver.Screen isn't assignable
+ //driver.Screen = new (0, 0, driver.Cols, Rows);
+ Assert.Equal (new (0, 0, 100, 30), driver.Screen);
+ Assert.NotEqual (new (0, 0, 100, 30), Application.Screen);
+ Assert.Equal (new (0, 0, 80, 25), Application.Screen);
+ Application.Screen = new (0, 0, driver.Cols, driver.Rows);
+ Assert.Equal (new (0, 0, 100, 30), driver.Screen);
+
+ Application.Shutdown ();
+ }
+
private void Init ()
{
Application.Init (new FakeDriver ());
diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs
index 47812926d2..db78ebe3b8 100644
--- a/UnitTests/Application/MainLoopTests.cs
+++ b/UnitTests/Application/MainLoopTests.cs
@@ -946,10 +946,14 @@ private class MillisecondTolerance : IEqualityComparer
private class TestMainloop : IMainLoopDriver
{
private MainLoop mainLoop;
+ private bool _forceRead1;
+ private ManualResetEventSlim _waitForInput1;
public bool EventsPending () { throw new NotImplementedException (); }
public void Iteration () { throw new NotImplementedException (); }
public void TearDown () { throw new NotImplementedException (); }
public void Setup (MainLoop mainLoop) { this.mainLoop = mainLoop; }
public void Wakeup () { throw new NotImplementedException (); }
+ public bool ForceRead { get; set; }
+ public ManualResetEventSlim WaitForInput { get; set; }
}
}
diff --git a/UnitTests/Application/SynchronizatonContextTests.cs b/UnitTests/Application/SynchronizatonContextTests.cs
index fce4a3250d..9f98046499 100644
--- a/UnitTests/Application/SynchronizatonContextTests.cs
+++ b/UnitTests/Application/SynchronizatonContextTests.cs
@@ -4,7 +4,7 @@ namespace Terminal.Gui.ApplicationTests;
public class SyncrhonizationContextTests
{
- [Fact(Skip = "Causes ubuntu to crash in github action.")]
+ [Fact]
public void SynchronizationContext_CreateCopy ()
{
Application.Init ();
@@ -20,11 +20,12 @@ public void SynchronizationContext_CreateCopy ()
[Theory]
[InlineData (typeof (FakeDriver))]
- //[InlineData (typeof (NetDriver))]
+ [InlineData (typeof (NetDriver))]
[InlineData (typeof (WindowsDriver))]
- //[InlineData (typeof (CursesDriver))]
+ [InlineData (typeof (CursesDriver))]
public void SynchronizationContext_Post (Type driverType)
{
+ ConsoleDriver.RunningUnitTests = true;
Application.Init (driverName: driverType.Name);
SynchronizationContext context = SynchronizationContext.Current;
diff --git a/UnitTests/Input/AnsiEscapeSequenceRequestUtilsTests.cs b/UnitTests/Input/AnsiEscapeSequenceRequestUtilsTests.cs
new file mode 100644
index 0000000000..a9c8f5d059
--- /dev/null
+++ b/UnitTests/Input/AnsiEscapeSequenceRequestUtilsTests.cs
@@ -0,0 +1,1566 @@
+using JetBrains.Annotations;
+// ReSharper disable HeuristicUnreachableCode
+
+namespace Terminal.Gui.InputTests;
+
+public class AnsiEscapeSequenceRequestUtilsTests
+{
+ private bool _actionStarted;
+ private MouseFlags _arg1;
+ private Point _arg2;
+ private string _c1Control, _code, _terminating;
+ private ConsoleKeyInfo [] _cki;
+ private bool _isKeyMouse;
+ [CanBeNull]
+ private AnsiEscapeSequenceRequestStatus _seqReqStatus;
+ private ConsoleKey _key;
+ private ConsoleModifiers _mod;
+ private List _mouseFlags;
+ private ConsoleKeyInfo _newConsoleKeyInfo;
+ private Point _pos;
+ private string [] _values;
+
+ [Fact]
+ [AutoInitShutdown]
+ public void DecodeEscSeq_Multiple_Tests ()
+ {
+ // ESC
+ _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false) };
+ var expectedCki = new ConsoleKeyInfo ('\u001b', ConsoleKey.Escape, false, false, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.Escape, _key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("ESC", _c1Control);
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Null (_terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+ _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false), new ('\u0012', 0, false, false, false) };
+ expectedCki = new ('\u0012', ConsoleKey.R, false, true, true);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.R, _key);
+ Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod);
+ Assert.Equal ("ESC", _c1Control);
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Equal ("\u0012", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+ _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false), new ('r', 0, false, false, false) };
+ expectedCki = new ('r', ConsoleKey.R, false, true, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.R, _key);
+ Assert.Equal (ConsoleModifiers.Alt, _mod);
+ Assert.Equal ("ESC", _c1Control);
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Equal ("r", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ // SS3
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false), new ('O', 0, false, false, false), new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, false, false, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("SS3", _c1Control);
+ Assert.Null (_code);
+ Assert.Single (_values);
+ Assert.Null (_values [0]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ // CSI
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, true, false, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Shift, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("2", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, false, true, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Alt, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('4', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, true, true, false);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("4", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('5', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, false, false, true);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Control, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("5", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('6', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, true, false, true);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Control, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("6", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('7', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, false, true, true);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("7", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('8', 0, false, false, false),
+ new ('R', 0, false, false, false)
+ };
+ expectedCki = new ('\0', ConsoleKey.F3, true, true, true);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.F3, _key);
+ Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("1", _values [0]);
+ Assert.Equal ("8", _values [^1]);
+ Assert.Equal ("R", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("M", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (new () { MouseFlags.Button1Pressed }, _mouseFlags);
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('m', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("m", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (2, _mouseFlags.Count);
+
+ Assert.Equal (
+ new () { MouseFlags.Button1Released, MouseFlags.Button1Clicked },
+ _mouseFlags
+ );
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("M", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (new () { MouseFlags.Button1DoubleClicked }, _mouseFlags);
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("M", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (new () { MouseFlags.Button1TripleClicked }, _mouseFlags);
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+
+ var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), WantContinuousButtonPressed = true };
+ var top = new Toplevel ();
+ top.Add (view);
+ Application.Begin (top);
+
+ Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("M", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (new () { MouseFlags.Button1Pressed }, _mouseFlags);
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+
+ Application.Iteration += (s, a) =>
+ {
+ if (_actionStarted)
+ {
+ // set Application.WantContinuousButtonPressedView to null
+ view.WantContinuousButtonPressed = false;
+
+ Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
+
+ Application.RequestStop ();
+ }
+ };
+
+ Application.Run (top);
+ top.Dispose ();
+
+ Assert.Null (Application.WantContinuousButtonPressedView);
+
+ Assert.Equal (MouseFlags.Button1Pressed, _arg1);
+ Assert.Equal (new (1, 2), _arg2);
+
+ ClearAll ();
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('m', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Equal ("<", _code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("0", _values [0]);
+ Assert.Equal ("2", _values [1]);
+ Assert.Equal ("3", _values [^1]);
+ Assert.Equal ("m", _terminating);
+ Assert.True (_isKeyMouse);
+ Assert.Equal (new () { MouseFlags.Button1Released }, _mouseFlags);
+ Assert.Equal (new (1, 2), _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ AnsiEscapeSequenceRequests.Clear ();
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+
+ _cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('8', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new ('t', 0, false, false, false)
+ };
+ expectedCki = default (ConsoleKeyInfo);
+ Assert.Single (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (0, (int)_key);
+ Assert.Equal (0, (int)_mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("8", _values [0]);
+ Assert.Equal ("10", _values [1]);
+ Assert.Equal ("20", _values [^1]);
+ Assert.Equal ("t", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.NotNull (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+ }
+
+ [Theory]
+ [InlineData ('A', ConsoleKey.A, true, true, false, "ESC", '\u001b', 'A')]
+ [InlineData ('a', ConsoleKey.A, false, true, false, "ESC", '\u001b', 'a')]
+ [InlineData ('\0', ConsoleKey.Spacebar, false, true, true, "ESC", '\u001b', '\0')]
+ [InlineData (' ', ConsoleKey.Spacebar, true, true, false, "ESC", '\u001b', ' ')]
+ [InlineData ('\n', ConsoleKey.Enter, false, true, true, "ESC", '\u001b', '\n')]
+ [InlineData ('\r', ConsoleKey.Enter, true, true, false, "ESC", '\u001b', '\r')]
+ public void DecodeEscSeq_More_Multiple_Tests (
+ char keyChar,
+ ConsoleKey consoleKey,
+ bool shift,
+ bool alt,
+ bool control,
+ string c1Control,
+ params char [] kChars
+ )
+ {
+ _cki = new ConsoleKeyInfo [kChars.Length];
+
+ for (var i = 0; i < kChars.Length; i++)
+ {
+ char kChar = kChars [i];
+ _cki [i] = new (kChar, 0, false, false, false);
+ }
+
+ var expectedCki = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (consoleKey, _key);
+
+ ConsoleModifiers mods = new ();
+
+ if (shift)
+ {
+ mods = ConsoleModifiers.Shift;
+ }
+
+ if (alt)
+ {
+ mods |= ConsoleModifiers.Alt;
+ }
+
+ if (control)
+ {
+ mods |= ConsoleModifiers.Control;
+ }
+
+ Assert.Equal (mods, _mod);
+ Assert.Equal (c1Control, _c1Control);
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Equal (keyChar.ToString (), _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+ }
+
+ [Fact]
+ public void DecodeEscSeq_IncompleteCKInfos ()
+ {
+ // This is simulated response from a CSI_ReportTerminalSizeInChars
+ _cki =
+ [
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('8', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ ];
+
+ ConsoleKeyInfo expectedCki = default;
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.None, _key);
+ Assert.Equal (ConsoleModifiers.None, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal ([0], _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+ Assert.Equal (_cki, AnsiEscapeSequenceRequestUtils.IncompleteCkInfos);
+
+ _cki = AnsiEscapeSequenceRequestUtils.InsertArray (
+ AnsiEscapeSequenceRequestUtils.IncompleteCkInfos,
+ [
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new ('t', 0, false, false, false)
+ ]);
+
+ expectedCki = default;
+
+ // Add a request to avoid assert failure in the DecodeEscSeq method
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (ConsoleKey.None, _key);
+
+ Assert.Equal (ConsoleModifiers.None, _mod);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (3, _values.Length);
+ Assert.Equal ("t", _terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal ([0], _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ AnsiEscapeSequenceRequests.HasResponse ("t", out _seqReqStatus);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+ Assert.NotEqual (_cki, AnsiEscapeSequenceRequestUtils.IncompleteCkInfos);
+ Assert.Contains (AnsiEscapeSequenceRequestUtils.ToString (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos), AnsiEscapeSequenceRequestUtils.ToString (_cki));
+
+ ClearAll ();
+ }
+
+ [Theory]
+ [InlineData ('\u001B', ConsoleKey.Escape, false, false, false)]
+ [InlineData ('\r', ConsoleKey.Enter, false, false, false)]
+ [InlineData ('1', ConsoleKey.D1, false, false, false)]
+ [InlineData ('!', ConsoleKey.None, false, false, false)]
+ [InlineData ('a', ConsoleKey.A, false, false, false)]
+ [InlineData ('A', ConsoleKey.A, true, false, false)]
+ [InlineData ('\u0001', ConsoleKey.A, false, false, true)]
+ [InlineData ('\0', ConsoleKey.Spacebar, false, false, true)]
+ [InlineData ('\n', ConsoleKey.Enter, false, false, true)]
+ [InlineData ('\t', ConsoleKey.Tab, false, false, false)]
+ public void DecodeEscSeq_Single_Tests (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
+ {
+ _cki = [new (keyChar, 0, false, false, false)];
+ var expectedCki = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
+
+ AnsiEscapeSequenceRequestUtils.DecodeEscSeq (
+ ref _newConsoleKeyInfo,
+ ref _key,
+ _cki,
+ ref _mod,
+ out _c1Control,
+ out _code,
+ out _values,
+ out _terminating,
+ out _isKeyMouse,
+ out _mouseFlags,
+ out _pos,
+ out _seqReqStatus,
+ ProcessContinuousButtonPressed
+ );
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal (expectedCki, _newConsoleKeyInfo);
+ Assert.Equal (consoleKey, _key);
+
+ ConsoleModifiers mods = new ();
+
+ if (shift)
+ {
+ mods = ConsoleModifiers.Shift;
+ }
+
+ if (alt)
+ {
+ mods |= ConsoleModifiers.Alt;
+ }
+
+ if (control)
+ {
+ mods |= ConsoleModifiers.Control;
+ }
+
+ Assert.Equal (mods, _mod);
+
+ if (keyChar == '\u001B')
+ {
+ Assert.Equal ("ESC", _c1Control);
+ }
+ else
+ {
+ Assert.Null (_c1Control);
+ }
+
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Null (_terminating);
+ Assert.False (_isKeyMouse);
+ Assert.Equal (new () { 0 }, _mouseFlags);
+ Assert.Equal (Point.Empty, _pos);
+ Assert.Null (_seqReqStatus);
+ Assert.Equal (0, (int)_arg1);
+ Assert.Equal (Point.Empty, _arg2);
+
+ ClearAll ();
+ }
+
+ [Fact]
+ public void Defaults_Values ()
+ {
+ Assert.Equal ('\x1b', AnsiEscapeSequenceRequestUtils.KeyEsc);
+ Assert.Equal ("\x1b[", AnsiEscapeSequenceRequestUtils.CSI);
+ Assert.Equal ("\x1b[?1003h", AnsiEscapeSequenceRequestUtils.CSI_EnableAnyEventMouse);
+ Assert.Equal ("\x1b[?1006h", AnsiEscapeSequenceRequestUtils.CSI_EnableSgrExtModeMouse);
+ Assert.Equal ("\x1b[?1015h", AnsiEscapeSequenceRequestUtils.CSI_EnableUrxvtExtModeMouse);
+ Assert.Equal ("\x1b[?1003l", AnsiEscapeSequenceRequestUtils.CSI_DisableAnyEventMouse);
+ Assert.Equal ("\x1b[?1006l", AnsiEscapeSequenceRequestUtils.CSI_DisableSgrExtModeMouse);
+ Assert.Equal ("\x1b[?1015l", AnsiEscapeSequenceRequestUtils.CSI_DisableUrxvtExtModeMouse);
+ Assert.Equal ("\x1b[?1003h\x1b[?1015h\u001b[?1006h", AnsiEscapeSequenceRequestUtils.CSI_EnableMouseEvents);
+ Assert.Equal ("\x1b[?1003l\x1b[?1015l\u001b[?1006l", AnsiEscapeSequenceRequestUtils.CSI_DisableMouseEvents);
+ }
+
+ [Fact]
+ public void GetC1ControlChar_Tests ()
+ {
+ Assert.Equal ("IND", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('D'));
+ Assert.Equal ("NEL", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('E'));
+ Assert.Equal ("HTS", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('H'));
+ Assert.Equal ("RI", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('M'));
+ Assert.Equal ("SS2", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('N'));
+ Assert.Equal ("SS3", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('O'));
+ Assert.Equal ("DCS", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('P'));
+ Assert.Equal ("SPA", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('V'));
+ Assert.Equal ("EPA", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('W'));
+ Assert.Equal ("SOS", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('X'));
+ Assert.Equal ("DECID", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('Z'));
+ Assert.Equal ("CSI", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('['));
+ Assert.Equal ("ST", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('\\'));
+ Assert.Equal ("OSC", AnsiEscapeSequenceRequestUtils.GetC1ControlChar (']'));
+ Assert.Equal ("PM", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('^'));
+ Assert.Equal ("APC", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('_'));
+ Assert.Equal ("", AnsiEscapeSequenceRequestUtils.GetC1ControlChar ('\0'));
+ }
+
+ [Fact]
+ public void GetConsoleInputKey_ConsoleKeyInfo ()
+ {
+ var cki = new ConsoleKeyInfo ('r', 0, false, false, false);
+ var expectedCki = new ConsoleKeyInfo ('r', ConsoleKey.R, false, false, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, true, false, false);
+ expectedCki = new ('r', ConsoleKey.R, true, false, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, false, true, false);
+ expectedCki = new ('r', ConsoleKey.R, false, true, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, false, false, true);
+ expectedCki = new ('r', ConsoleKey.R, false, false, true);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, true, true, false);
+ expectedCki = new ('r', ConsoleKey.R, true, true, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, false, true, true);
+ expectedCki = new ('r', ConsoleKey.R, false, true, true);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('r', 0, true, true, true);
+ expectedCki = new ('r', ConsoleKey.R, true, true, true);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('\u0012', 0, false, false, false);
+ expectedCki = new ('\u0012', ConsoleKey.R, false, false, true);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('\0', (ConsoleKey)64, false, false, true);
+ expectedCki = new ('\0', ConsoleKey.Spacebar, false, false, true);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('\r', 0, false, false, false);
+ expectedCki = new ('\r', ConsoleKey.Enter, false, false, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('\u007f', 0, false, false, false);
+ expectedCki = new ('\u007f', ConsoleKey.Backspace, false, false, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+
+ cki = new ('R', 0, false, false, false);
+ expectedCki = new ('R', ConsoleKey.R, true, false, false);
+ Assert.Equal (expectedCki, AnsiEscapeSequenceRequestUtils.MapConsoleKeyInfo (cki));
+ }
+
+ [Fact]
+ public void GetConsoleKey_Tests ()
+ {
+ ConsoleModifiers mod = 0;
+ char keyChar = '\0';
+ Assert.Equal (ConsoleKey.UpArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('A', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.DownArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('B', "", ref mod, ref keyChar));
+ Assert.Equal (_key = ConsoleKey.RightArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('C', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.LeftArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('D', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.End, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('F', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Home, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('H', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F1, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('P', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F2, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('Q', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F3, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('R', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F4, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('S', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Tab, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('Z', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleModifiers.Shift, mod);
+ Assert.Equal (0, (int)AnsiEscapeSequenceRequestUtils.GetConsoleKey ('\0', "", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Insert, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "2", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Delete, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "3", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.PageUp, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "5", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.PageDown, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "6", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F5, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "15", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F6, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "17", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F7, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "18", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F8, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "19", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F9, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "20", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F10, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "21", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F11, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "23", ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.F12, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "24", ref mod, ref keyChar));
+ Assert.Equal (0, (int)AnsiEscapeSequenceRequestUtils.GetConsoleKey ('~', "", ref mod, ref keyChar));
+ // These terminators are used by macOS on a numeric keypad without keys modifiers
+ Assert.Equal (ConsoleKey.Add, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('l', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Subtract, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('m', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Insert, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('p', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.End, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('q', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.DownArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('r', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.PageDown, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('s', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.LeftArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('t', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Clear, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('u', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.RightArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('v', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.Home, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('w', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.UpArrow, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('x', null, ref mod, ref keyChar));
+ Assert.Equal (ConsoleKey.PageUp, AnsiEscapeSequenceRequestUtils.GetConsoleKey ('y', null, ref mod, ref keyChar));
+ }
+
+ [Fact]
+ public void GetConsoleModifiers_Tests ()
+ {
+ Assert.Equal (ConsoleModifiers.Shift, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("2"));
+ Assert.Equal (ConsoleModifiers.Alt, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("3"));
+ Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("4"));
+ Assert.Equal (ConsoleModifiers.Control, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("5"));
+ Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Control, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("6"));
+ Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("7"));
+
+ Assert.Equal (
+ ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
+ AnsiEscapeSequenceRequestUtils.GetConsoleModifiers ("8")
+ );
+ Assert.Equal (0, (int)AnsiEscapeSequenceRequestUtils.GetConsoleModifiers (""));
+ }
+
+ [Fact]
+ public void GetEscapeResult_Multiple_Tests ()
+ {
+ char [] kChars = ['\u001b', '[', '5', ';', '1', '0', 'r'];
+ (_c1Control, _code, _values, _terminating) = AnsiEscapeSequenceRequestUtils.GetEscapeResult (kChars);
+ Assert.Equal ("CSI", _c1Control);
+ Assert.Null (_code);
+ Assert.Equal (2, _values.Length);
+ Assert.Equal ("5", _values [0]);
+ Assert.Equal ("10", _values [^1]);
+ Assert.Equal ("r", _terminating);
+ }
+
+ [Theory]
+ [InlineData ('\u001B')]
+ [InlineData (['\r'])]
+ [InlineData (['1'])]
+ [InlineData (['!'])]
+ [InlineData (['a'])]
+ [InlineData (['A'])]
+ public void GetEscapeResult_Single_Tests (params char [] kChars)
+ {
+ (_c1Control, _code, _values, _terminating) = AnsiEscapeSequenceRequestUtils.GetEscapeResult (kChars);
+
+ if (kChars [0] == '\u001B')
+ {
+ Assert.Equal ("ESC", _c1Control);
+ }
+ else
+ {
+ Assert.Null (_c1Control);
+ }
+
+ Assert.Null (_code);
+ Assert.Null (_values);
+ Assert.Null (_terminating);
+ }
+
+ [Fact]
+ public void GetKeyCharArray_Tests ()
+ {
+ ConsoleKeyInfo [] cki =
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('5', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('1', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new ('r', 0, false, false, false)
+ };
+
+ Assert.Equal (new [] { '\u001b', '[', '5', ';', '1', '0', 'r' }, AnsiEscapeSequenceRequestUtils.GetKeyCharArray (cki));
+ }
+
+ [Fact]
+ [AutoInitShutdown]
+ public void GetMouse_Tests ()
+ {
+ ConsoleKeyInfo [] cki =
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ AnsiEscapeSequenceRequestUtils.GetMouse (cki, out List mouseFlags, out Point pos, ProcessContinuousButtonPressed);
+ Assert.Equal (new () { MouseFlags.Button1Pressed }, mouseFlags);
+ Assert.Equal (new (1, 2), pos);
+
+ cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('m', 0, false, false, false)
+ };
+ AnsiEscapeSequenceRequestUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
+ Assert.Equal (2, mouseFlags.Count);
+
+ Assert.Equal (
+ new () { MouseFlags.Button1Released, MouseFlags.Button1Clicked },
+ mouseFlags
+ );
+ Assert.Equal (new (1, 2), pos);
+
+ cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ AnsiEscapeSequenceRequestUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
+ Assert.Equal (new () { MouseFlags.Button1DoubleClicked }, mouseFlags);
+ Assert.Equal (new (1, 2), pos);
+
+ cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('M', 0, false, false, false)
+ };
+ AnsiEscapeSequenceRequestUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
+ Assert.Equal (new () { MouseFlags.Button1TripleClicked }, mouseFlags);
+ Assert.Equal (new (1, 2), pos);
+
+ cki = new ConsoleKeyInfo []
+ {
+ new ('\u001b', 0, false, false, false),
+ new ('[', 0, false, false, false),
+ new ('<', 0, false, false, false),
+ new ('0', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('2', 0, false, false, false),
+ new (';', 0, false, false, false),
+ new ('3', 0, false, false, false),
+ new ('m', 0, false, false, false)
+ };
+ AnsiEscapeSequenceRequestUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
+ Assert.Equal (new () { MouseFlags.Button1Released }, mouseFlags);
+ Assert.Equal (new (1, 2), pos);
+ }
+
+ [Fact]
+ public void ResizeArray_ConsoleKeyInfo ()
+ {
+ ConsoleKeyInfo [] expectedCkInfos = null;
+ var cki = new ConsoleKeyInfo ('\u001b', ConsoleKey.Escape, false, false, false);
+ expectedCkInfos = AnsiEscapeSequenceRequestUtils.ResizeArray (cki, expectedCkInfos);
+ Assert.Single (expectedCkInfos);
+ Assert.Equal (cki, expectedCkInfos [0]);
+ }
+
+ [Theory]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\b", "\b")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\t", "\t")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\n", "\n")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\r", "\r")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCe", "e")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCV", "V")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\u007f", "\u007f")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC ", " ")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\\", "\\")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC|", "|")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC1", "1")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC!", "!")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\"", "\"")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC@", "@")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC#", "#")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC£", "£")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC$", "$")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC§", "§")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC%", "%")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC€", "€")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC&", "&")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC/", "/")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC{", "{")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC(", "(")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC[", "[")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC)", ")")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC]", "]")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC=", "=")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC}", "}")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC'", "'")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC?", "?")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC«", "«")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC»", "»")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC+", "+")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC*", "*")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC¨", "¨")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC´", "´")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC`", "`")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCç", "ç")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCº", "º")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCª", "ª")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC~", "~")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC^", "^")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC<", "<")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC>", ">")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC,", ",")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC;", ";")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC.", ".")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC:", ":")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC-", "-")]
+ [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC_", "_")]
+ public void SplitEscapeRawString_Multiple_Tests (string rawData, string expectedLast)
+ {
+ List splitList = AnsiEscapeSequenceRequestUtils.SplitEscapeRawString (rawData);
+ Assert.Equal (18, splitList.Count);
+ Assert.Equal ("\r", splitList [0]);
+ Assert.Equal ("\u001b[<35;50;1m", splitList [1]);
+ Assert.Equal ("\u001b[<35;49;1m", splitList [2]);
+ Assert.Equal ("\u001b[<35;47;1m", splitList [3]);
+ Assert.Equal ("\u001b[<35;46;1m", splitList [4]);
+ Assert.Equal ("\u001b[<35;45;2m", splitList [5]);
+ Assert.Equal ("\u001b[<35;44;2m", splitList [6]);
+ Assert.Equal ("\u001b[<35;43;3m", splitList [7]);
+ Assert.Equal ("\u001b[<35;42;3m", splitList [8]);
+ Assert.Equal ("\u001b[<35;41;4m", splitList [9]);
+ Assert.Equal ("\u001b[<35;40;5m", splitList [10]);
+ Assert.Equal ("\u001b[<35;39;6m", splitList [11]);
+ Assert.Equal ("\u001b[<35;49;1m", splitList [12]);
+ Assert.Equal ("\u001b[<35;48;2m", splitList [13]);
+ Assert.Equal ("\u001b[<0;33;6M", splitList [14]);
+ Assert.Equal ("\u001b[<0;33;6m", splitList [15]);
+ Assert.Equal ("\u001bOC", splitList [16]);
+ Assert.Equal (expectedLast, splitList [^1]);
+ }
+
+ [Theory]
+ [InlineData ("[<35;50;1m")]
+ [InlineData ("\r")]
+ [InlineData ("1")]
+ [InlineData ("!")]
+ [InlineData ("a")]
+ [InlineData ("A")]
+ public void SplitEscapeRawString_Single_Tests (string rawData)
+ {
+ List splitList = AnsiEscapeSequenceRequestUtils.SplitEscapeRawString (rawData);
+ Assert.Single (splitList);
+ Assert.Equal (rawData, splitList [0]);
+ }
+
+ [Theory]
+ [InlineData (null, null, null, null)]
+ [InlineData ("\u001b[8;1", null, null, "\u001b[8;1")]
+ [InlineData (null, "\u001b[8;1", 5, "\u001b[8;1")]
+ [InlineData ("\u001b[8;1", null, 5, "\u001b[8;1")]
+ [InlineData ("\u001b[8;1", "0;20t", -1, "\u001b[8;10;20t")]
+ [InlineData ("\u001b[8;1", "0;20t", 0, "\u001b[8;10;20t")]
+ [InlineData ("0;20t", "\u001b[8;1", 5, "\u001b[8;10;20t")]
+ [InlineData ("0;20t", "\u001b[8;1", 3, "\u001b[80;20t;1")]
+ public void InsertArray_Tests (string toInsert, string current, int? index, string expected)
+ {
+ ConsoleKeyInfo [] toIns = AnsiEscapeSequenceRequestUtils.ToConsoleKeyInfoArray (toInsert);
+ ConsoleKeyInfo [] cki = AnsiEscapeSequenceRequestUtils.ToConsoleKeyInfoArray (current);
+ ConsoleKeyInfo [] result = AnsiEscapeSequenceRequestUtils.ToConsoleKeyInfoArray (expected);
+
+ if (index is null)
+ {
+ cki = AnsiEscapeSequenceRequestUtils.InsertArray (toIns, cki);
+ }
+ else
+ {
+ cki = AnsiEscapeSequenceRequestUtils.InsertArray (toIns, cki, (int)index);
+ }
+
+ Assert.Equal (result, cki);
+ }
+
+ private void ClearAll ()
+ {
+ AnsiEscapeSequenceRequests.Clear ();
+ _newConsoleKeyInfo = default (ConsoleKeyInfo);
+ _key = default (ConsoleKey);
+ _cki = default (ConsoleKeyInfo []);
+ _mod = default (ConsoleModifiers);
+ _c1Control = default (string);
+ _code = default (string);
+ _terminating = default (string);
+ _values = default (string []);
+ _isKeyMouse = default (bool);
+ _seqReqStatus = null;
+ _mouseFlags = default (List);
+ _pos = default (Point);
+ _arg1 = default (MouseFlags);
+ _arg2 = default (Point);
+ }
+
+ private void ProcessContinuousButtonPressed (MouseFlags arg1, Point arg2)
+ {
+ _arg1 = arg1;
+ _arg2 = arg2;
+ _actionStarted = true;
+ }
+}
diff --git a/UnitTests/Input/AnsiEscapeSequenceRequestsTests.cs b/UnitTests/Input/AnsiEscapeSequenceRequestsTests.cs
new file mode 100644
index 0000000000..1658fb4699
--- /dev/null
+++ b/UnitTests/Input/AnsiEscapeSequenceRequestsTests.cs
@@ -0,0 +1,79 @@
+namespace Terminal.Gui.InputTests;
+
+public class AnsiEscapeSequenceRequestsTests
+{
+ [Fact]
+ public void Add_Tests ()
+ {
+ AnsiEscapeSequenceRequests.Clear ();
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ Assert.Single (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ Assert.Equal (2, AnsiEscapeSequenceRequests.Statuses.Count);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+
+ AnsiEscapeSequenceRequests.Clear ();
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ Assert.Equal (2, AnsiEscapeSequenceRequests.Statuses.Count);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ Assert.Equal (3, AnsiEscapeSequenceRequests.Statuses.Count);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+ }
+
+ [Fact]
+ public void Constructor_Defaults ()
+ {
+ AnsiEscapeSequenceRequests.Clear ();
+ Assert.NotNull (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ }
+
+ [Fact]
+ public void Remove_Tests ()
+ {
+ AnsiEscapeSequenceRequests.Clear ();
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ AnsiEscapeSequenceRequests.HasResponse ("t", out AnsiEscapeSequenceRequestStatus seqReqStatus);
+ AnsiEscapeSequenceRequests.Remove (seqReqStatus);
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ AnsiEscapeSequenceRequests.HasResponse ("t", out seqReqStatus);
+ AnsiEscapeSequenceRequests.Remove (seqReqStatus);
+ Assert.Single (AnsiEscapeSequenceRequests.Statuses);
+ Assert.Equal ("t", AnsiEscapeSequenceRequests.Statuses.ToArray () [^1].AnsiRequest.Terminator);
+
+ AnsiEscapeSequenceRequests.HasResponse ("t", out seqReqStatus);
+ AnsiEscapeSequenceRequests.Remove (seqReqStatus);
+ Assert.Empty (AnsiEscapeSequenceRequests.Statuses);
+ }
+
+ [Fact]
+ public void Requested_Tests ()
+ {
+ AnsiEscapeSequenceRequests.Clear ();
+ Assert.False (AnsiEscapeSequenceRequests.HasResponse ("t", out AnsiEscapeSequenceRequestStatus seqReqStatus));
+ Assert.Null (seqReqStatus);
+
+ AnsiEscapeSequenceRequests.Add (new () { Request = "", Terminator = "t" });
+ Assert.False (AnsiEscapeSequenceRequests.HasResponse ("r", out seqReqStatus));
+ Assert.NotNull (seqReqStatus);
+ Assert.Equal ("t", seqReqStatus.AnsiRequest.Terminator);
+ Assert.True (AnsiEscapeSequenceRequests.HasResponse ("t", out seqReqStatus));
+ Assert.NotNull (seqReqStatus);
+ Assert.Equal ("t", seqReqStatus.AnsiRequest.Terminator);
+ }
+
+ [Fact]
+ public void Request_Initialization_AnsiEscapeSequenceResponse_Is_Null ()
+ {
+ AnsiEscapeSequenceRequest ansiRequest = new () { Request = "\u001b[0c", Terminator = "c"};
+ Assert.Null (ansiRequest.AnsiEscapeSequenceResponse);
+ }
+}
diff --git a/UnitTests/Input/EscSeqReqTests.cs b/UnitTests/Input/EscSeqReqTests.cs
deleted file mode 100644
index 6b73b2af0f..0000000000
--- a/UnitTests/Input/EscSeqReqTests.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-namespace Terminal.Gui.InputTests;
-
-public class EscSeqReqTests
-{
- [Fact]
- public void Add_Tests ()
- {
- var escSeqReq = new EscSeqRequests ();
- escSeqReq.Add ("t");
- Assert.Single (escSeqReq.Statuses);
- Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
- Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests);
- Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
- escSeqReq.Add ("t", 2);
- Assert.Single (escSeqReq.Statuses);
- Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
- Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests);
- Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
- escSeqReq = new EscSeqRequests ();
- escSeqReq.Add ("t", 2);
- Assert.Single (escSeqReq.Statuses);
- Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
- Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
- Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding);
-
- escSeqReq.Add ("t", 3);
- Assert.Single (escSeqReq.Statuses);
- Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
- Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
- Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding);
- }
-
- [Fact]
- public void Constructor_Defaults ()
- {
- var escSeqReq = new EscSeqRequests ();
- Assert.NotNull (escSeqReq.Statuses);
- Assert.Empty (escSeqReq.Statuses);
- }
-
- [Fact]
- public void Remove_Tests ()
- {
- var escSeqReq = new EscSeqRequests ();
- escSeqReq.Add ("t");
- escSeqReq.Remove ("t");
- Assert.Empty (escSeqReq.Statuses);
-
- escSeqReq.Add ("t", 2);
- escSeqReq.Remove ("t");
- Assert.Single (escSeqReq.Statuses);
- Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator);
- Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests);
- Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding);
-
- escSeqReq.Remove ("t");
- Assert.Empty (escSeqReq.Statuses);
- }
-
- [Fact]
- public void Requested_Tests ()
- {
- var escSeqReq = new EscSeqRequests ();
- Assert.False (escSeqReq.HasResponse ("t"));
-
- escSeqReq.Add ("t");
- Assert.False (escSeqReq.HasResponse ("r"));
- Assert.True (escSeqReq.HasResponse ("t"));
- }
-}
diff --git a/UnitTests/Input/EscSeqUtilsTests.cs b/UnitTests/Input/EscSeqUtilsTests.cs
deleted file mode 100644
index 9b811f0026..0000000000
--- a/UnitTests/Input/EscSeqUtilsTests.cs
+++ /dev/null
@@ -1,1184 +0,0 @@
-namespace Terminal.Gui.InputTests;
-
-public class EscSeqUtilsTests
-{
- private bool _actionStarted;
- private MouseFlags _arg1;
- private Point _arg2;
- private string _c1Control, _code, _terminating;
- private ConsoleKeyInfo [] _cki;
- private EscSeqRequests _escSeqReqProc;
- private bool _isKeyMouse;
- private bool _isReq;
- private ConsoleKey _key;
- private ConsoleModifiers _mod;
- private List _mouseFlags;
- private ConsoleKeyInfo _newConsoleKeyInfo;
- private Point _pos;
- private string [] _values;
-
- [Fact]
- [AutoInitShutdown]
- public void DecodeEscSeq_Tests ()
- {
- // ESC
- _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false) };
- var expectedCki = new ConsoleKeyInfo ('\u001b', ConsoleKey.Escape, false, false, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.Escape, _key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("ESC", _c1Control);
- Assert.Null (_code);
- Assert.Null (_values);
- Assert.Null (_terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
- _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false), new ('\u0012', 0, false, false, false) };
- expectedCki = new ('\u0012', ConsoleKey.R, false, true, true);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.R, _key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("ESC", _c1Control);
- Assert.Null (_code);
- Assert.Null (_values);
- Assert.Equal ("\u0012", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
- _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false), new ('r', 0, false, false, false) };
- expectedCki = new ('R', ConsoleKey.R, false, true, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.R, _key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("ESC", _c1Control);
- Assert.Null (_code);
- Assert.Null (_values);
- Assert.Equal ("r", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- // SS3
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false), new ('O', 0, false, false, false), new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, false, false, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("SS3", _c1Control);
- Assert.Null (_code);
- Assert.Single (_values);
- Assert.Null (_values [0]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- // CSI
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, true, false, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Shift, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("2", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, false, true, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Alt, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('4', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, true, true, false);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("4", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('5', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, false, false, true);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Control, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("5", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('6', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, true, false, true);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Control, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("6", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('7', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, false, true, true);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("7", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('1', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('8', 0, false, false, false),
- new ('R', 0, false, false, false)
- };
- expectedCki = new ('\0', ConsoleKey.F3, true, true, true);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (ConsoleKey.F3, _key);
- Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("1", _values [0]);
- Assert.Equal ("8", _values [^1]);
- Assert.Equal ("R", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("M", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (new() { MouseFlags.Button1Pressed }, _mouseFlags);
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('m', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("m", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (2, _mouseFlags.Count);
-
- Assert.Equal (
- new() { MouseFlags.Button1Released, MouseFlags.Button1Clicked },
- _mouseFlags
- );
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("M", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (new() { MouseFlags.Button1DoubleClicked }, _mouseFlags);
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("M", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (new() { MouseFlags.Button1TripleClicked }, _mouseFlags);
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
-
- var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), WantContinuousButtonPressed = true };
- var top = new Toplevel ();
- top.Add (view);
- Application.Begin (top);
-
- Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("M", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (new() { MouseFlags.Button1Pressed }, _mouseFlags);
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
-
- Application.Iteration += (s, a) =>
- {
- if (_actionStarted)
- {
- // set Application.WantContinuousButtonPressedView to null
- view.WantContinuousButtonPressed = false;
-
- Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
-
- Application.RequestStop ();
- }
- };
-
- Application.Run (top);
- top.Dispose ();
-
- Assert.Null (Application.WantContinuousButtonPressedView);
-
- Assert.Equal (MouseFlags.Button1Pressed, _arg1);
- Assert.Equal (new (1, 2), _arg2);
-
- ClearAll ();
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('m', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Null (_escSeqReqProc);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Equal ("<", _code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("0", _values [0]);
- Assert.Equal ("2", _values [1]);
- Assert.Equal ("3", _values [^1]);
- Assert.Equal ("m", _terminating);
- Assert.True (_isKeyMouse);
- Assert.Equal (new() { MouseFlags.Button1Released }, _mouseFlags);
- Assert.Equal (new (1, 2), _pos);
- Assert.False (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
-
- ClearAll ();
-
- Assert.Null (_escSeqReqProc);
- _escSeqReqProc = new ();
- _escSeqReqProc.Add ("t");
-
- _cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('8', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('1', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new ('0', 0, false, false, false),
- new ('t', 0, false, false, false)
- };
- expectedCki = default (ConsoleKeyInfo);
- Assert.Single (_escSeqReqProc.Statuses);
- Assert.Equal ("t", _escSeqReqProc.Statuses [^1].Terminator);
-
- EscSeqUtils.DecodeEscSeq (
- _escSeqReqProc,
- ref _newConsoleKeyInfo,
- ref _key,
- _cki,
- ref _mod,
- out _c1Control,
- out _code,
- out _values,
- out _terminating,
- out _isKeyMouse,
- out _mouseFlags,
- out _pos,
- out _isReq,
- ProcessContinuousButtonPressed
- );
- Assert.Empty (_escSeqReqProc.Statuses);
- Assert.Equal (expectedCki, _newConsoleKeyInfo);
- Assert.Equal (0, (int)_key);
- Assert.Equal (0, (int)_mod);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (3, _values.Length);
- Assert.Equal ("8", _values [0]);
- Assert.Equal ("10", _values [1]);
- Assert.Equal ("20", _values [^1]);
- Assert.Equal ("t", _terminating);
- Assert.False (_isKeyMouse);
- Assert.Equal (new() { 0 }, _mouseFlags);
- Assert.Equal (Point.Empty, _pos);
- Assert.True (_isReq);
- Assert.Equal (0, (int)_arg1);
- Assert.Equal (Point.Empty, _arg2);
- }
-
- [Fact]
- public void Defaults_Values ()
- {
- Assert.Equal ('\x1b', EscSeqUtils.KeyEsc);
- Assert.Equal ("\x1b[", EscSeqUtils.CSI);
- Assert.Equal ("\x1b[?1003h", EscSeqUtils.CSI_EnableAnyEventMouse);
- Assert.Equal ("\x1b[?1006h", EscSeqUtils.CSI_EnableSgrExtModeMouse);
- Assert.Equal ("\x1b[?1015h", EscSeqUtils.CSI_EnableUrxvtExtModeMouse);
- Assert.Equal ("\x1b[?1003l", EscSeqUtils.CSI_DisableAnyEventMouse);
- Assert.Equal ("\x1b[?1006l", EscSeqUtils.CSI_DisableSgrExtModeMouse);
- Assert.Equal ("\x1b[?1015l", EscSeqUtils.CSI_DisableUrxvtExtModeMouse);
- Assert.Equal ("\x1b[?1003h\x1b[?1015h\u001b[?1006h", EscSeqUtils.CSI_EnableMouseEvents);
- Assert.Equal ("\x1b[?1003l\x1b[?1015l\u001b[?1006l", EscSeqUtils.CSI_DisableMouseEvents);
- }
-
- [Fact]
- public void GetC1ControlChar_Tests ()
- {
- Assert.Equal ("IND", EscSeqUtils.GetC1ControlChar ('D'));
- Assert.Equal ("NEL", EscSeqUtils.GetC1ControlChar ('E'));
- Assert.Equal ("HTS", EscSeqUtils.GetC1ControlChar ('H'));
- Assert.Equal ("RI", EscSeqUtils.GetC1ControlChar ('M'));
- Assert.Equal ("SS2", EscSeqUtils.GetC1ControlChar ('N'));
- Assert.Equal ("SS3", EscSeqUtils.GetC1ControlChar ('O'));
- Assert.Equal ("DCS", EscSeqUtils.GetC1ControlChar ('P'));
- Assert.Equal ("SPA", EscSeqUtils.GetC1ControlChar ('V'));
- Assert.Equal ("EPA", EscSeqUtils.GetC1ControlChar ('W'));
- Assert.Equal ("SOS", EscSeqUtils.GetC1ControlChar ('X'));
- Assert.Equal ("DECID", EscSeqUtils.GetC1ControlChar ('Z'));
- Assert.Equal ("CSI", EscSeqUtils.GetC1ControlChar ('['));
- Assert.Equal ("ST", EscSeqUtils.GetC1ControlChar ('\\'));
- Assert.Equal ("OSC", EscSeqUtils.GetC1ControlChar (']'));
- Assert.Equal ("PM", EscSeqUtils.GetC1ControlChar ('^'));
- Assert.Equal ("APC", EscSeqUtils.GetC1ControlChar ('_'));
- Assert.Equal ("", EscSeqUtils.GetC1ControlChar ('\0'));
- }
-
- [Fact]
- public void GetConsoleInputKey_ConsoleKeyInfo ()
- {
- var cki = new ConsoleKeyInfo ('r', 0, false, false, false);
- var expectedCki = new ConsoleKeyInfo ('r', 0, false, false, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, true, false, false);
- expectedCki = new ('r', 0, true, false, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, false, true, false);
- expectedCki = new ('r', 0, false, true, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, false, false, true);
- expectedCki = new ('r', 0, false, false, true);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, true, true, false);
- expectedCki = new ('r', 0, true, true, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, false, true, true);
- expectedCki = new ('r', 0, false, true, true);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('r', 0, true, true, true);
- expectedCki = new ('r', 0, true, true, true);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('\u0012', 0, false, false, false);
- expectedCki = new ('R', ConsoleKey.R, false, false, true);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('\0', (ConsoleKey)64, false, false, true);
- expectedCki = new (' ', ConsoleKey.Spacebar, false, false, true);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('\r', 0, false, false, false);
- expectedCki = new ('\r', ConsoleKey.Enter, false, false, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('\u007f', 0, false, false, false);
- expectedCki = new ('\u007f', ConsoleKey.Backspace, false, false, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
-
- cki = new ('R', 0, false, false, false);
- expectedCki = new ('R', 0, false, false, false);
- Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki));
- }
-
- [Fact]
- public void GetConsoleKey_Tests ()
- {
- ConsoleModifiers mod = 0;
- char keyChar = '\0';
- Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('A', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('B', "", ref mod, ref keyChar));
- Assert.Equal (_key = ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('C', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('D', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('F', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('H', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F1, EscSeqUtils.GetConsoleKey ('P', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F2, EscSeqUtils.GetConsoleKey ('Q', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F3, EscSeqUtils.GetConsoleKey ('R', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F4, EscSeqUtils.GetConsoleKey ('S', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Tab, EscSeqUtils.GetConsoleKey ('Z', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleModifiers.Shift, mod);
- Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('\0', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('~', "2", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Delete, EscSeqUtils.GetConsoleKey ('~', "3", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('~', "5", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('~', "6", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F5, EscSeqUtils.GetConsoleKey ('~', "15", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F6, EscSeqUtils.GetConsoleKey ('~', "17", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F7, EscSeqUtils.GetConsoleKey ('~', "18", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F8, EscSeqUtils.GetConsoleKey ('~', "19", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F9, EscSeqUtils.GetConsoleKey ('~', "20", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F10, EscSeqUtils.GetConsoleKey ('~', "21", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F11, EscSeqUtils.GetConsoleKey ('~', "23", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.F12, EscSeqUtils.GetConsoleKey ('~', "24", ref mod, ref keyChar));
- Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('~', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Add, EscSeqUtils.GetConsoleKey ('l', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Subtract, EscSeqUtils.GetConsoleKey ('m', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('p', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('q', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('r', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('s', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('t', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Clear, EscSeqUtils.GetConsoleKey ('u', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('v', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('w', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('x', "", ref mod, ref keyChar));
- Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('y', "", ref mod, ref keyChar));
- }
-
- [Fact]
- public void GetConsoleModifiers_Tests ()
- {
- Assert.Equal (ConsoleModifiers.Shift, EscSeqUtils.GetConsoleModifiers ("2"));
- Assert.Equal (ConsoleModifiers.Alt, EscSeqUtils.GetConsoleModifiers ("3"));
- Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt, EscSeqUtils.GetConsoleModifiers ("4"));
- Assert.Equal (ConsoleModifiers.Control, EscSeqUtils.GetConsoleModifiers ("5"));
- Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Control, EscSeqUtils.GetConsoleModifiers ("6"));
- Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, EscSeqUtils.GetConsoleModifiers ("7"));
-
- Assert.Equal (
- ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
- EscSeqUtils.GetConsoleModifiers ("8")
- );
- Assert.Equal (0, (int)EscSeqUtils.GetConsoleModifiers (""));
- }
-
- [Fact]
- public void GetEscapeResult_Tests ()
- {
- char [] kChars = { '\u001b', '[', '5', ';', '1', '0', 'r' };
- (_c1Control, _code, _values, _terminating) = EscSeqUtils.GetEscapeResult (kChars);
- Assert.Equal ("CSI", _c1Control);
- Assert.Null (_code);
- Assert.Equal (2, _values.Length);
- Assert.Equal ("5", _values [0]);
- Assert.Equal ("10", _values [^1]);
- Assert.Equal ("r", _terminating);
- }
-
- [Fact]
- public void GetKeyCharArray_Tests ()
- {
- ConsoleKeyInfo [] cki =
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('5', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('1', 0, false, false, false),
- new ('0', 0, false, false, false),
- new ('r', 0, false, false, false)
- };
-
- Assert.Equal (new [] { '\u001b', '[', '5', ';', '1', '0', 'r' }, EscSeqUtils.GetKeyCharArray (cki));
- }
-
- [Fact]
- [AutoInitShutdown]
- public void GetMouse_Tests ()
- {
- ConsoleKeyInfo [] cki =
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- EscSeqUtils.GetMouse (cki, out List mouseFlags, out Point pos, ProcessContinuousButtonPressed);
- Assert.Equal (new() { MouseFlags.Button1Pressed }, mouseFlags);
- Assert.Equal (new (1, 2), pos);
-
- cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('m', 0, false, false, false)
- };
- EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
- Assert.Equal (2, mouseFlags.Count);
-
- Assert.Equal (
- new() { MouseFlags.Button1Released, MouseFlags.Button1Clicked },
- mouseFlags
- );
- Assert.Equal (new (1, 2), pos);
-
- cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
- Assert.Equal (new() { MouseFlags.Button1DoubleClicked }, mouseFlags);
- Assert.Equal (new (1, 2), pos);
-
- cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('M', 0, false, false, false)
- };
- EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
- Assert.Equal (new() { MouseFlags.Button1TripleClicked }, mouseFlags);
- Assert.Equal (new (1, 2), pos);
-
- cki = new ConsoleKeyInfo []
- {
- new ('\u001b', 0, false, false, false),
- new ('[', 0, false, false, false),
- new ('<', 0, false, false, false),
- new ('0', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('2', 0, false, false, false),
- new (';', 0, false, false, false),
- new ('3', 0, false, false, false),
- new ('m', 0, false, false, false)
- };
- EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed);
- Assert.Equal (new() { MouseFlags.Button1Released }, mouseFlags);
- Assert.Equal (new (1, 2), pos);
- }
-
- [Fact]
- public void ResizeArray_ConsoleKeyInfo ()
- {
- ConsoleKeyInfo [] expectedCkInfos = null;
- var cki = new ConsoleKeyInfo ('\u001b', ConsoleKey.Escape, false, false, false);
- expectedCkInfos = EscSeqUtils.ResizeArray (cki, expectedCkInfos);
- Assert.Single (expectedCkInfos);
- Assert.Equal (cki, expectedCkInfos [0]);
- }
-
- private void ClearAll ()
- {
- _escSeqReqProc = default (EscSeqRequests);
- _newConsoleKeyInfo = default (ConsoleKeyInfo);
- _key = default (ConsoleKey);
- _cki = default (ConsoleKeyInfo []);
- _mod = default (ConsoleModifiers);
- _c1Control = default (string);
- _code = default (string);
- _terminating = default (string);
- _values = default (string []);
- _isKeyMouse = default (bool);
- _isReq = default (bool);
- _mouseFlags = default (List);
- _pos = default (Point);
- _arg1 = default (MouseFlags);
- _arg2 = default (Point);
- }
-
- private void ProcessContinuousButtonPressed (MouseFlags arg1, Point arg2)
- {
- _arg1 = arg1;
- _arg2 = arg2;
- _actionStarted = true;
- }
-}
diff --git a/UnitTests/Input/KeyTests.cs b/UnitTests/Input/KeyTests.cs
index fa2695e5f9..cf4cb78585 100644
--- a/UnitTests/Input/KeyTests.cs
+++ b/UnitTests/Input/KeyTests.cs
@@ -342,10 +342,8 @@ public void Standard_Keys_Should_Equal_KeyCode ()
[InlineData ((KeyCode)'{', "{")]
[InlineData ((KeyCode)'\'', "\'")]
[InlineData ((KeyCode)'ó', "ó")]
- [InlineData (
- (KeyCode)'Ó' | KeyCode.ShiftMask,
- "Shift+Ó"
- )] // TODO: This is not correct, it should be Shift+ó or just Ó
+ [InlineData ((KeyCode)'Ó' | KeyCode.ShiftMask, "Shift+Ó")]
+ [InlineData ((KeyCode)'ó' | KeyCode.ShiftMask, "Shift+ó")]
[InlineData ((KeyCode)'Ó', "Ó")]
[InlineData ((KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Shift+ç")]
[InlineData ((KeyCode)'a', "a")] // 97 or Key.Space | Key.A
diff --git a/UnitTests/View/Adornment/PaddingTests.cs b/UnitTests/View/Adornment/PaddingTests.cs
index 0242bcc1da..a4defc9972 100644
--- a/UnitTests/View/Adornment/PaddingTests.cs
+++ b/UnitTests/View/Adornment/PaddingTests.cs
@@ -33,5 +33,7 @@ P P
output
);
TestHelpers.AssertDriverAttributesAre ("0", output, null, view.GetNormalColor ());
+
+ ((FakeDriver)Application.Driver!).End ();
}
}