diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
index 662736e93..2e8da6bfe 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceRequest.cs
@@ -4,35 +4,38 @@ namespace Terminal.Gui;
///
/// Describes an ongoing ANSI request sent to the console.
/// Use to handle the response
-/// when console answers the request.
+/// when the console answers the request.
///
public class AnsiEscapeSequenceRequest
{
internal readonly object _responseLock = new (); // Per-instance lock
///
- /// Request to send e.g. see
+ /// Gets the request string to send e.g. see
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Request
///
///
public required string Request { get; init; }
+ // QUESTION: Could the type of this propperty be AnsiEscapeSequenceResponse? This would remove the
+ // QUESTION: removal of the redundant Rresponse, Terminator, and ExpectedRespnseValue properties from this class?
+ // QUESTION: Does string.Empty indicate no response recevied? If not, perhaps make this property nullable?
///
- /// Response received from the request.
+ /// Gets the response received from the request.
///
public string Response { get; internal set; } = string.Empty;
///
- /// Invoked when the console responds with an ANSI response code that matches the
+ /// Raised when the console responds with an ANSI response code that matches the
///
///
public event EventHandler? ResponseReceived;
///
///
- /// The terminator that uniquely identifies the type of response as responded
- /// by the console. e.g. for
+ /// Gets the terminator that uniquely identifies the response received from
+ /// the console. e.g. for
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Request
///
@@ -50,15 +53,15 @@ public class AnsiEscapeSequenceRequest
public required string Terminator { get; init; }
///
- /// Execute an ANSI escape sequence escape which may return a response or error.
+ /// Attempt an ANSI escape sequence request which may return a response or error.
///
/// The ANSI escape sequence to request.
///
- /// When this method returns , an object containing the response with an empty
- /// error.
+ /// When this method returns , the response. will
+ /// be .
///
- /// A with the response, error, terminator and value.
- public static bool TryExecuteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
+ /// A with the response, error, terminator, and value.
+ public static bool TryRequest (AnsiEscapeSequenceRequest ansiRequest, out AnsiEscapeSequenceResponse result)
{
var error = new StringBuilder ();
var values = new string? [] { null };
@@ -72,7 +75,7 @@ public class AnsiEscapeSequenceRequest
if (!string.IsNullOrEmpty (ansiRequest.Response) && !ansiRequest.Response.StartsWith (EscSeqUtils.KeyEsc))
{
- throw new InvalidOperationException ("Invalid escape character!");
+ throw new InvalidOperationException ($"Invalid Response: {ansiRequest.Response}");
}
if (string.IsNullOrEmpty (ansiRequest.Terminator))
@@ -102,7 +105,7 @@ public class AnsiEscapeSequenceRequest
AnsiEscapeSequenceResponse ansiResponse = new ()
{
Response = ansiRequest.Response, Error = error.ToString (),
- Terminator = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response [^1].ToString (), Value = values [0]
+ Terminator = string.IsNullOrEmpty (ansiRequest.Response) ? "" : ansiRequest.Response [^1].ToString (), ExpectedResponseValue = values [0]
};
// Invoke the event if it's subscribed
@@ -114,16 +117,17 @@ public class AnsiEscapeSequenceRequest
}
///
- /// The value expected in the response e.g.
+ /// The value expected in the response after the CSI e.g.
///
/// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
///
- /// which will have a 't' as terminator but also other different request may return the same terminator with a
- /// different value.
+ /// should result in a response of the form ESC [ 8 ; height ; width t. In this case,
+ /// will be "8".
///
- public string? Value { get; init; }
+ public string? ExpectedResponseValue { get; init; }
internal void RaiseResponseFromInput (AnsiEscapeSequenceRequest ansiRequest, string response) { ResponseFromInput?.Invoke (ansiRequest, response); }
+ // QUESTION: What is this for? Please provide a descriptive comment.
internal event EventHandler? ResponseFromInput;
}
diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
index df9851155..2cc820801 100644
--- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
+++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence/AnsiEscapeSequenceResponse.cs
@@ -2,20 +2,23 @@
namespace Terminal.Gui;
///
-/// Describes a finished ANSI received from the console.
+/// Describes a response received from the console as a result of a request being sent via .
///
public class AnsiEscapeSequenceResponse
{
+ // QUESTION: Should this be nullable to indicate there was no error, or is string.Empty sufficient?
///
- /// Error received from e.g. see
+ /// Gets the error string received from e.g. see
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Request
///
+ /// .
///
public required string Error { get; init; }
+ // QUESTION: Does string.Empty indicate no response recevied? If not, perhaps make this property nullable?
///
- /// Response received from e.g. see
+ /// Gets the Response string received from e.g. see
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Request
///
@@ -23,10 +26,11 @@ public class AnsiEscapeSequenceResponse
///
public required string Response { get; init; }
+ // QUESTION: Does string.Empty indicate no terminator expected? If not, perhaps make this property nullable?
///
///
- /// The terminator that uniquely identifies the type of response as responded
- /// by the console. e.g. for
+ /// Gets the terminator that uniquely identifies the response received from
+ /// the console. e.g. for
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Request
///
@@ -34,20 +38,23 @@ public class AnsiEscapeSequenceResponse
///
/// EscSeqUtils.CSI_SendDeviceAttributes.Terminator
///
+ /// .
///
///
- /// The received terminator must match to the terminator sent by the request.
+ /// After sending a request, the first response with matching terminator will be matched
+ /// to the oldest outstanding request.
///
///
public required string Terminator { get; init; }
///
- /// The value expected in the response e.g.
+ /// The value expected in the response after the CSI e.g.
///
/// EscSeqUtils.CSI_ReportTerminalSizeInChars.Value
///
- /// which will have a 't' as terminator but also other different request may return the same terminator with a
- /// different value.
+ /// should result in a response of the form ESC [ 8 ; height ; width t. In this case,
+ /// will be "8".
///
- public string? Value { get; init; }
+
+ public string? ExpectedResponseValue { get; init; }
}
diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
index b37c50c53..d6ccf9991 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
@@ -1,7 +1,4 @@
#nullable enable
-//
-// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
-//
using System.Diagnostics;
@@ -15,6 +12,53 @@ namespace Terminal.Gui;
///
public abstract class ConsoleDriver
{
+ ///
+ /// 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; }
+
+ /// 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 ();
+
+ #region ANSI Esc Sequence Handling
+
+ // QUESTION: Should this be virtual with a default implementation that does the common stuff?
+ // QUESTION: Looking at the implementations of this method, there is TONs of duplicated code.
+ // QUESTION: We should figure out how to find just the things that are unique to each driver and
+ // QUESTION: create more fine-grained APIs to handle those.
+ ///
+ /// Provide handling for the terminal write ANSI escape sequence request.
+ ///
+ /// The object.
+ /// The request response.
+ public abstract string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest);
+
+ // QUESTION: This appears to be an API to help in debugging. It's only implemented in CursesDriver and WindowsDriver.
+ // QUESTION: Can it be factored such that it does not contaminate the ConsoleDriver API?
+ ///
+ /// Provide proper writing to send escape sequence recognized by the .
+ ///
+ ///
+ public abstract void WriteRaw (string ansi);
+
+ #endregion ANSI Esc Sequence Handling
+
+ #region Screen and Contents
+
// As performance is a concern, we keep track of the dirty lines and only refresh those.
// This is in addition to the dirty flag on each cell.
internal bool []? _dirtyLines;
@@ -23,30 +67,18 @@ public abstract class ConsoleDriver
/// Gets the location and size of the terminal screen.
internal Rectangle Screen => new (0, 0, Cols, Rows);
- private Rectangle _clip;
+ /// Redraws the physical screen with the contents that have been queued up via any of the printing commands.
+ public abstract void UpdateScreen ();
- ///
- /// Gets or sets the clip rectangle that and are subject
- /// to.
- ///
- /// The rectangle describing the of region.
- public Rectangle Clip
- {
- get => _clip;
- set
- {
- if (_clip == value)
- {
- return;
- }
+ /// Called when the terminal size changes. Fires the event.
+ ///
+ public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
- // Don't ever let Clip be bigger than Screen
- _clip = Rectangle.Intersect (Screen, value);
- }
- }
+ /// The event fired when the terminal is resized.
+ public event EventHandler? SizeChanged;
- /// Get the operating system clipboard.
- public IClipboard? Clipboard { get; internal set; }
+ /// Updates the screen to reflect all the changes that have been done to the display buffer
+ public abstract void Refresh ();
///
/// Gets the column last set by . and are used by
@@ -75,6 +107,43 @@ public abstract class ConsoleDriver
/// The leftmost column in the terminal.
public virtual int Left { get; internal 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.
@@ -95,16 +164,27 @@ public abstract class ConsoleDriver
/// The topmost row in the terminal.
public virtual int Top { get; internal set; } = 0;
+ private Rectangle _clip;
+
///
- /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver.
- ///
- /// public ColorTests ()
- /// {
- /// ConsoleDriver.RunningUnitTests = true;
- /// }
- ///
+ /// Gets or sets the clip rectangle that and are subject
+ /// to.
///
- internal static bool RunningUnitTests { get; set; }
+ /// The rectangle describing the of region.
+ public Rectangle Clip
+ {
+ get => _clip;
+ set
+ {
+ if (_clip == value)
+ {
+ return;
+ }
+
+ // Don't ever let Clip be bigger than Screen
+ _clip = Rectangle.Intersect (Screen, value);
+ }
+ }
/// Adds the specified rune to the display at the current cursor position.
///
@@ -310,10 +390,38 @@ public abstract class ConsoleDriver
}
}
+ /// 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
+ public void FillRect (Rectangle rect, Rune rune = default)
+ {
+ rect = Rectangle.Intersect (rect, Clip);
+
+ lock (Contents!)
+ {
+ for (int r = rect.Y; r < rect.Y + rect.Height; r++)
+ {
+ for (int c = rect.X; c < rect.X + rect.Width; c++)
+ {
+ Contents [r, c] = new ()
+ {
+ Rune = rune != default (Rune) ? rune : (Rune)' ',
+ Attribute = CurrentAttribute, IsDirty = true
+ };
+ _dirtyLines! [r] = true;
+ }
+ }
+ }
+ }
+
/// Clears the of the driver.
public void ClearContents ()
{
Contents = new Cell [Rows, Cols];
+
//CONCURRENCY: Unsynchronized access to Clip isn't safe.
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
Clip = Screen;
@@ -325,21 +433,22 @@ public abstract class ConsoleDriver
{
for (var c = 0; c < Cols; c++)
{
- Contents [row, c] = new Cell
+ Contents [row, c] = new ()
{
Rune = (Rune)' ',
Attribute = new Attribute (Color.White, Color.Black),
IsDirty = true
};
}
+
_dirtyLines [row] = true;
}
}
}
///
- /// Sets as dirty for situations where views
- /// don't need layout and redrawing, but just refresh the screen.
+ /// Sets as dirty for situations where views
+ /// don't need layout and redrawing, but just refresh the screen.
///
public void SetContentsAsDirty ()
{
@@ -351,41 +460,12 @@ public abstract class ConsoleDriver
{
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
- public void FillRect (Rectangle rect, Rune rune = default)
- {
- rect = Rectangle.Intersect (rect, Clip);
- lock (Contents!)
- {
- for (int r = rect.Y; r < rect.Y + rect.Height; r++)
- {
- for (int c = rect.X; c < rect.X + rect.Width; c++)
- {
- Contents [r, c] = new Cell
- {
- Rune = rune != default ? rune : (Rune)' ',
- Attribute = CurrentAttribute, IsDirty = true
- };
- _dirtyLines! [r] = true;
- }
- }
- }
- }
-
///
/// Fills the specified rectangle with the specified . This method is a convenience method
/// that calls .
@@ -394,79 +474,28 @@ public abstract class ConsoleDriver
///
public 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 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;
- }
-
- /// Called when the terminal size changes. Fires the event.
- ///
- public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); }
-
- /// Updates the screen to reflect all the changes that have been done to the display buffer
- public abstract void Refresh ();
+ /// Sets the position of the terminal cursor to and .
+ public abstract void UpdateCursor ();
/// Sets the terminal cursor visibility.
/// The wished
/// upon success
public abstract bool SetCursorVisibility (CursorVisibility visibility);
- /// The event fired when the terminal is resized.
- public event EventHandler? SizeChanged;
-
- /// 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 ();
-
- /// Sets the position of the terminal cursor to and .
- public abstract void UpdateCursor ();
-
- /// Redraws the physical screen with the contents that have been queued up via any of the printing commands.
- public abstract void UpdateScreen ();
+ #endregion Cursor Handling
#region Setup & Teardown
@@ -518,7 +547,7 @@ public abstract class ConsoleDriver
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
if (Application.Driver is { })
{
- _currentAttribute = new Attribute (value.Foreground, value.Background);
+ _currentAttribute = new (value.Foreground, value.Background);
return;
}
@@ -551,16 +580,33 @@ public abstract class ConsoleDriver
public virtual Attribute MakeColor (in Color foreground, in Color background)
{
// Encode the colors into the int value.
- return new Attribute (
- -1, // only used by cursesdriver!
- foreground,
- background
- );
+ return new (
+ -1, // only used by cursesdriver!
+ foreground,
+ background
+ );
}
- #endregion
+ #endregion Color Handling
- #region Mouse and Keyboard
+ #region Mouse Handling
+
+ /// 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);
+ }
+
+ #endregion Mouse Handling
+
+ #region Keyboard Handling
/// Event fired when a key is pressed down. This is a precursor to .
public event EventHandler? KeyDown;
@@ -587,19 +633,8 @@ public abstract class ConsoleDriver
///
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.
@@ -608,337 +643,5 @@ public abstract class ConsoleDriver
/// If simulates the Ctrl key being pressed.
public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl);
- ///
- /// Provide handling for the terminal write ANSI escape sequence request.
- ///
- /// The object.
- /// The request response.
- public abstract string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest);
-
- ///
- /// Provide proper writing to send escape sequence recognized by the .
- ///
- ///
- public abstract void WriteRaw (string ansi);
-
- #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 503e7cd57..6fce2e040 100644
--- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
+++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs
@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
namespace Terminal.Gui.ConsoleDrivers;
+// QUESTION: This class combines Windows specific code with cross-platform code. Should this be split into two classes?
/// Helper class to handle the scan code and virtual key from a .
public static class ConsoleKeyMapping
{
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
index 4568bcf46..e0225b3aa 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
@@ -1,4 +1,5 @@
-//
+// TODO: #nullable enable
+//
// Driver.cs: Curses-based Driver
//
@@ -9,14 +10,36 @@ 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 libary.
internal class CursesDriver : ConsoleDriver
{
- public Curses.Window _window;
- private CursorVisibility? _currentCursorVisibility;
- private CursorVisibility? _initialCursorVisibility;
- private MouseFlags _lastMouseFlags;
- private UnixMainLoop _mainLoopDriver;
+ public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; }
+
+ public override void Refresh ()
+ {
+ UpdateScreen ();
+ UpdateCursor ();
+ }
+
+ public override void Suspend ()
+ {
+ StopReportingMouseMoves ();
+
+ if (!RunningUnitTests)
+ {
+ Platform.Suspend ();
+
+ if (Force16Colors)
+ {
+ Curses.Window.Standard.redrawwin ();
+ Curses.refresh ();
+ }
+ }
+
+ StartReportingMouseMoves ();
+ }
+
+ #region Screen and Contents
public override int Cols
{
@@ -38,59 +61,6 @@ internal class CursesDriver : ConsoleDriver
}
}
- public override bool SupportsTrueColor => true;
-
- ///
- 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;
- }
-
- 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
@@ -118,202 +88,6 @@ internal class CursesDriver : ConsoleDriver
}
}
- public override void Refresh ()
- {
- UpdateScreen ();
- UpdateCursor ();
- }
-
- public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
- {
- KeyCode key;
-
- if (consoleKey == ConsoleKey.Packet)
- {
- var mod = new ConsoleModifiers ();
-
- if (shift)
- {
- mod |= ConsoleModifiers.Shift;
- }
-
- if (alt)
- {
- mod |= ConsoleModifiers.Alt;
- }
-
- if (control)
- {
- mod |= ConsoleModifiers.Control;
- }
-
- var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
- cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
- key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
- }
- else
- {
- key = (KeyCode)keyChar;
- }
-
- OnKeyDown (new Key (key));
- OnKeyUp (new Key (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);
- }
- }
-
- public void StopReportingMouseMoves ()
- {
- if (!RunningUnitTests)
- {
- Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
- }
- }
-
- private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
- private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
-
- ///
- public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
- {
- if (_mainLoopDriver is null)
- {
- return string.Empty;
- }
-
- try
- {
- lock (ansiRequest._responseLock)
- {
- ansiRequest.ResponseFromInput += (s, e) =>
- {
- Debug.Assert (s == ansiRequest);
- Debug.Assert (e == ansiRequest.Response);
-
- _waitAnsiResponse.Set ();
- };
-
- _mainLoopDriver.EscSeqRequests.Add (ansiRequest, this);
-
- _mainLoopDriver._forceRead = true;
- }
-
- if (!_ansiResponseTokenSource.IsCancellationRequested)
- {
- _mainLoopDriver._waitForInput.Set ();
-
- _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- return string.Empty;
- }
-
- lock (ansiRequest._responseLock)
- {
- _mainLoopDriver._forceRead = false;
-
- if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
- {
- if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0
- && string.IsNullOrEmpty (request.AnsiRequest.Response))
- {
- lock (request!.AnsiRequest._responseLock)
- {
- // Bad request or no response at all
- _mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _);
- }
- }
- }
-
- _waitAnsiResponse.Reset ();
-
- return ansiRequest.Response;
- }
- }
-
- ///
- public override void WriteRaw (string ansi)
- {
- _mainLoopDriver.WriteRaw (ansi);
- }
-
- public override void Suspend ()
- {
- StopReportingMouseMoves ();
-
- if (!RunningUnitTests)
- {
- Platform.Suspend ();
-
- if (Force16Colors)
- {
- Curses.Window.Standard.redrawwin ();
- Curses.refresh ();
- }
- }
-
- StartReportingMouseMoves ();
- }
-
- public override void UpdateCursor ()
- {
- EnsureCursorVisibility ();
-
- if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
- {
- if (Force16Colors)
- {
- Curses.move (Row, Col);
-
- Curses.raw ();
- Curses.noecho ();
- Curses.refresh ();
- }
- else
- {
- _mainLoopDriver.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
- }
- }
- }
-
public override void UpdateScreen ()
{
if (Force16Colors)
@@ -507,10 +281,10 @@ internal class CursesDriver : ConsoleDriver
}
// 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);
@@ -528,258 +302,12 @@ internal class CursesDriver : ConsoleDriver
}
}
- 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 (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
-
- return true;
- }
-
- internal override void End ()
- {
- _ansiResponseTokenSource?.Cancel ();
- _ansiResponseTokenSource?.Dispose ();
- _waitAnsiResponse?.Dispose ();
-
- StopReportingMouseMoves ();
- SetCursorVisibility (CursorVisibility.Default);
-
- if (RunningUnitTests)
- {
- return;
- }
-
- // throws away any typeahead that has been typed by
- // the user and has not yet been read by the program.
- Curses.flushinp ();
-
- Curses.endwin ();
- }
-
- internal override MainLoop Init ()
- {
- _mainLoopDriver = new UnixMainLoop (this);
-
- if (!RunningUnitTests)
- {
- _window = Curses.initscr ();
- Curses.set_escdelay (10);
-
- // Ensures that all procedures are performed at some previous closing.
- Curses.doupdate ();
-
- //
- // We are setting Invisible as default, so we could ignore XTerm DECSUSR setting
- //
- switch (Curses.curs_set (0))
- {
- case 0:
- _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
-
- break;
-
- case 1:
- _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
- Curses.curs_set (1);
-
- break;
-
- case 2:
- _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
- Curses.curs_set (2);
-
- break;
-
- default:
- _currentCursorVisibility = _initialCursorVisibility = null;
-
- break;
- }
-
- if (!Curses.HasColors)
- {
- throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
- }
-
- Curses.raw ();
- Curses.noecho ();
-
- Curses.Window.Standard.keypad (true);
-
- Curses.StartColor ();
- Curses.UseDefaultColors ();
-
- if (!RunningUnitTests)
- {
- Curses.timeout (0);
- }
- }
-
- CurrentAttribute = new Attribute (ColorName16.White, ColorName16.Black);
-
- if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- {
- Clipboard = new FakeDriver.FakeClipboard ();
- }
- else
- {
- if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
- {
- Clipboard = new MacOSXClipboard ();
- }
- else
- {
- if (Is_WSL_Platform ())
- {
- Clipboard = new WSLClipboard ();
- }
- else
- {
- Clipboard = new CursesClipboard ();
- }
- }
- }
-
- ClearContents ();
- StartReportingMouseMoves ();
-
- if (!RunningUnitTests)
- {
- Curses.CheckWinChange ();
- ClearContents ();
-
- if (Force16Colors)
- {
- Curses.refresh ();
- }
- }
-
- return new MainLoop (_mainLoopDriver);
- }
-
- internal void ProcessInput (UnixMainLoop.PollData inputEvent)
- {
- switch (inputEvent.EventType)
- {
- case UnixMainLoop.EventType.Key:
- ConsoleKeyInfo consoleKeyInfo = inputEvent.KeyEvent;
-
- KeyCode map = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
-
- if (map == KeyCode.Null)
- {
- break;
- }
-
- OnKeyDown (new Key (map));
- OnKeyUp (new Key (map));
-
- break;
- case UnixMainLoop.EventType.Mouse:
- MouseEventArgs me = new MouseEventArgs { Position = inputEvent.MouseEvent.Position, Flags = inputEvent.MouseEvent.MouseFlags };
- OnMouseEvent (me);
-
- break;
- case UnixMainLoop.EventType.WindowSize:
- Size size = new (inputEvent.WindowSizeEvent.Size.Width, inputEvent.WindowSizeEvent.Size.Height);
- ProcessWinChange (inputEvent.WindowSizeEvent.Size);
-
- break;
- default:
- throw new ArgumentOutOfRangeException ();
- }
- }
-
- private void ProcessWinChange (Size size)
- {
- if (!RunningUnitTests && Curses.ChangeWindowSize (size.Height, size.Width))
- {
- ClearContents ();
- OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
- }
- }
-
- 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;
- }
- }
+ #endregion Screen and Contents
#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)
@@ -792,11 +320,11 @@ internal class CursesDriver : ConsoleDriver
// TODO: for TrueColor - Use InitExtendedPair
Curses.InitColorPair (v, foreground, background);
- return new Attribute (
- Curses.ColorPair (v),
- CursesColorNumberToColorName16 (foreground),
- CursesColorNumberToColorName16 (background)
- );
+ return new (
+ Curses.ColorPair (v),
+ CursesColorNumberToColorName16 (foreground),
+ CursesColorNumberToColorName16 (background)
+ );
}
///
@@ -815,11 +343,11 @@ internal class CursesDriver : ConsoleDriver
);
}
- return new Attribute (
- 0,
- foreground,
- background
- );
+ return new (
+ 0,
+ foreground,
+ background
+ );
}
private static short ColorNameToCursesColorNumber (ColorName16 color)
@@ -905,8 +433,501 @@ internal class CursesDriver : ConsoleDriver
}
#endregion
+
+ #region Cursor Support
+
+ 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;
+ }
+
+ ///
+ 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 override void UpdateCursor ()
+ {
+ EnsureCursorVisibility ();
+
+ if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
+ {
+ if (Force16Colors)
+ {
+ Curses.move (Row, Col);
+
+ Curses.raw ();
+ Curses.noecho ();
+ Curses.refresh ();
+ }
+ else
+ {
+ _mainLoopDriver.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
+ }
+ }
+ }
+
+ #endregion Cursor Support
+
+ #region Keyboard Support
+
+ public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control)
+ {
+ KeyCode key;
+
+ if (consoleKey == ConsoleKey.Packet)
+ {
+ var mod = new ConsoleModifiers ();
+
+ if (shift)
+ {
+ mod |= ConsoleModifiers.Shift;
+ }
+
+ if (alt)
+ {
+ mod |= ConsoleModifiers.Alt;
+ }
+
+ if (control)
+ {
+ mod |= ConsoleModifiers.Control;
+ }
+
+ var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
+ cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo);
+ key = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (cKeyInfo);
+ }
+ else
+ {
+ key = (KeyCode)keyChar;
+ }
+
+ OnKeyDown (new (key));
+ OnKeyUp (new (key));
+
+ //OnKeyPressed (new KeyEventArgsEventArgs (key));
+ }
+
+ // TODO: Unused- Remove
+ 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;
+ }
+ }
+
+ #endregion Keyboard Support
+
+ #region Mouse Support
+ public void StartReportingMouseMoves ()
+ {
+ if (!RunningUnitTests)
+ {
+ Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+ }
+ }
+
+ public void StopReportingMouseMoves ()
+ {
+ if (!RunningUnitTests)
+ {
+ Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+ }
+ }
+
+ #endregion Mouse Support
+
+ 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 (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
+
+ return true;
+ }
+
+ #region Init/End/MainLoop
+
+ public Curses.Window _window;
+ private UnixMainLoop _mainLoopDriver;
+
+ internal override MainLoop Init ()
+ {
+ _mainLoopDriver = new (this);
+
+ if (!RunningUnitTests)
+ {
+ _window = Curses.initscr ();
+ Curses.set_escdelay (10);
+
+ // Ensures that all procedures are performed at some previous closing.
+ Curses.doupdate ();
+
+ //
+ // We are setting Invisible as default, so we could ignore XTerm DECSUSR setting
+ //
+ switch (Curses.curs_set (0))
+ {
+ case 0:
+ _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Invisible;
+
+ break;
+
+ case 1:
+ _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Underline;
+ Curses.curs_set (1);
+
+ break;
+
+ case 2:
+ _currentCursorVisibility = _initialCursorVisibility = CursorVisibility.Box;
+ Curses.curs_set (2);
+
+ break;
+
+ default:
+ _currentCursorVisibility = _initialCursorVisibility = null;
+
+ break;
+ }
+
+ if (!Curses.HasColors)
+ {
+ throw new InvalidOperationException ("V2 - This should never happen. File an Issue if it does.");
+ }
+
+ Curses.raw ();
+ Curses.noecho ();
+
+ Curses.Window.Standard.keypad (true);
+
+ Curses.StartColor ();
+ Curses.UseDefaultColors ();
+
+ if (!RunningUnitTests)
+ {
+ Curses.timeout (0);
+ }
+ }
+
+ CurrentAttribute = new (ColorName16.White, ColorName16.Black);
+
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ {
+ Clipboard = new FakeDriver.FakeClipboard ();
+ }
+ else
+ {
+ if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
+ {
+ Clipboard = new MacOSXClipboard ();
+ }
+ else
+ {
+ if (Is_WSL_Platform ())
+ {
+ Clipboard = new WSLClipboard ();
+ }
+ else
+ {
+ Clipboard = new CursesClipboard ();
+ }
+ }
+ }
+
+ ClearContents ();
+ StartReportingMouseMoves ();
+
+ if (!RunningUnitTests)
+ {
+ Curses.CheckWinChange ();
+ ClearContents ();
+
+ if (Force16Colors)
+ {
+ Curses.refresh ();
+ }
+ }
+
+ return new (_mainLoopDriver);
+ }
+
+ internal void ProcessInput (UnixMainLoop.PollData inputEvent)
+ {
+ switch (inputEvent.EventType)
+ {
+ case UnixMainLoop.EventType.Key:
+ ConsoleKeyInfo consoleKeyInfo = inputEvent.KeyEvent;
+
+ KeyCode map = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
+
+ if (map == KeyCode.Null)
+ {
+ break;
+ }
+
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+
+ break;
+ case UnixMainLoop.EventType.Mouse:
+ var me = new MouseEventArgs { Position = inputEvent.MouseEvent.Position, Flags = inputEvent.MouseEvent.MouseFlags };
+ OnMouseEvent (me);
+
+ break;
+ case UnixMainLoop.EventType.WindowSize:
+ Size size = new (inputEvent.WindowSizeEvent.Size.Width, inputEvent.WindowSizeEvent.Size.Height);
+ ProcessWinChange (inputEvent.WindowSizeEvent.Size);
+
+ break;
+ default:
+ throw new ArgumentOutOfRangeException ();
+ }
+ }
+ private void ProcessWinChange (Size size)
+ {
+ if (!RunningUnitTests && Curses.ChangeWindowSize (size.Height, size.Width))
+ {
+ ClearContents ();
+ OnSizeChanged (new (new (Cols, Rows)));
+ }
+ }
+
+ internal override void End ()
+ {
+ _ansiResponseTokenSource?.Cancel ();
+ _ansiResponseTokenSource?.Dispose ();
+ _waitAnsiResponse?.Dispose ();
+
+ StopReportingMouseMoves ();
+ SetCursorVisibility (CursorVisibility.Default);
+
+ if (RunningUnitTests)
+ {
+ return;
+ }
+
+ // throws away any typeahead that has been typed by
+ // the user and has not yet been read by the program.
+ Curses.flushinp ();
+
+ Curses.endwin ();
+ }
+
+ #endregion Init/End/MainLoop
+
+ 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;
+ }
+ #region Low-Level Unix Stuff
+
+
+ private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
+ private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
+
+ ///
+ public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
+ {
+ if (_mainLoopDriver is null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ lock (ansiRequest._responseLock)
+ {
+ ansiRequest.ResponseFromInput += (s, e) =>
+ {
+ Debug.Assert (s == ansiRequest);
+ Debug.Assert (e == ansiRequest.Response);
+
+ _waitAnsiResponse.Set ();
+ };
+
+ _mainLoopDriver.EscSeqRequests.Add (ansiRequest, this);
+
+ _mainLoopDriver._forceRead = true;
+ }
+
+ if (!_ansiResponseTokenSource.IsCancellationRequested)
+ {
+ _mainLoopDriver._waitForInput.Set ();
+
+ _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return string.Empty;
+ }
+
+ lock (ansiRequest._responseLock)
+ {
+ _mainLoopDriver._forceRead = false;
+
+ if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
+ {
+ if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0
+ && string.IsNullOrEmpty (request.AnsiRequest.Response))
+ {
+ lock (request!.AnsiRequest._responseLock)
+ {
+ // Bad request or no response at all
+ _mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _);
+ }
+ }
+ }
+
+ _waitAnsiResponse.Reset ();
+
+ return ansiRequest.Response;
+ }
+ }
+
+ ///
+ public 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;
@@ -986,3 +1007,5 @@ internal static class Platform
[DllImport ("libc")]
private static extern int uname (nint buf);
}
+
+#endregion Low-Level Unix Stuff
diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c
index d289b7ef3..6ca9471d2 100644
--- a/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c
+++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c
@@ -1,11 +1,12 @@
#include
#include
-// This function is used to get the value of the TIOCGWINSZ variable,
+// Used to get the value of the TIOCGWINSZ variable,
// which may have different values on different Unix operating systems.
-// In Linux=0x005413, in Darwin and OpenBSD=0x40087468,
-// In Solaris=0x005468
-// The best solution is having a function that get the real value of the current OS
+// Linux=0x005413
+// Darwin and OpenBSD=0x40087468,
+// Solaris=0x005468
+// See https://stackoverflow.com/questions/16237137/what-is-termios-tiocgwinsz
int get_tiocgwinsz_value() {
return TIOCGWINSZ;
}
\ No newline at end of file
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
index c96c8f08b..280f3f9e7 100644
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
+++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
@@ -4,20 +4,7 @@ using System.Collections.Concurrent;
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 object.
- public EscSeqReqStatus (AnsiEscapeSequenceRequest ansiRequest) { AnsiRequest = ansiRequest; }
-
- /// Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator).
- public AnsiEscapeSequenceRequest AnsiRequest { get; }
-}
+// QUESTION: Can this class be moved/refactored/combined with the new AnsiEscapeSequenceRequest/Response class?
// TODO: This class is a singleton. It should use the singleton pattern.
///
diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs
new file mode 100644
index 000000000..c9561ec7b
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.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 EscSeqReqStatus
+{
+ /// Creates a new state of escape sequence request.
+ /// The object.
+ public EscSeqReqStatus (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/EscSeqUtils/EscSeqUtils.cs
index 4b283afb1..99e5bce8f 100644
--- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
+++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
@@ -3,6 +3,13 @@ using Terminal.Gui.ConsoleDrivers;
namespace Terminal.Gui;
+// QUESTION: Should this class be refactored into separate classes for:
+// QUESTION: CSI definitions
+// QUESTION: Primitives like DecodeEsqReq
+// QUESTION: Screen/Color/Cursor handling
+// QUESTION: Mouse handling
+// QUESTION: Keyboard handling
+
///
/// Provides a platform-independent API for managing ANSI escape sequences.
///
@@ -14,6 +21,7 @@ namespace Terminal.Gui;
///
public static class EscSeqUtils
{
+ // TODO: One type per file - Move this enum to a separate file.
///
/// Options for ANSI ESC "[xJ" - Clears part of the screen.
///
@@ -40,6 +48,9 @@ public static class EscSeqUtils
EntireScreenAndScrollbackBuffer = 3
}
+ // QUESTION: I wonder if EscSeqUtils.CSI_... should be more strongly typed such that this (and Terminator could be
+ // QUESTION: public required CSIRequests Request { get; init; }
+ // QUESTION: public required CSITerminators Terminator { get; init; }
///
/// Escape key code (ASCII 27/0x1B).
///
@@ -419,6 +430,7 @@ public static class EscSeqUtils
};
}
+
///
/// Gets the depending on terminating and value.
///
@@ -1721,7 +1733,7 @@ public static class EscSeqUtils
/// https://terminalguide.namepad.de/seq/csi_st-18/
/// The terminator indicating a reply to : ESC [ 8 ; height ; width t
///
- public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", Value = "8" };
+ public static readonly AnsiEscapeSequenceRequest CSI_ReportTerminalSizeInChars = new () { Request = CSI + "18t", Terminator = "t", ExpectedResponseValue = "8" };
#endregion
}
diff --git a/Terminal.Gui/ConsoleDrivers/KeyCode.cs b/Terminal.Gui/ConsoleDrivers/KeyCode.cs
new file mode 100644
index 000000000..183322ec8
--- /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 afa78ff6a..000000000
--- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs
+++ /dev/null
@@ -1,2007 +0,0 @@
-//
-// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
-//
-
-using System.Collections.Concurrent;
-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;
- internal readonly ManualResetEventSlim _waitForStart = new (false);
-
- //CancellationTokenSource _waitForStartCancellationTokenSource;
- private readonly ManualResetEventSlim _winChange = new (false);
- private readonly ConcurrentQueue _inputQueue = new ();
- private readonly ConsoleDriver _consoleDriver;
- private ConsoleKeyInfo [] _cki;
- private bool _isEscSeq;
-#if PROCESS_REQUEST
- bool _neededProcessRequest;
-#endif
- public EscSeqRequests EscSeqRequests { get; } = new ();
-
- public NetEvents (ConsoleDriver consoleDriver)
- {
- _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
- _inputReadyCancellationTokenSource = new CancellationTokenSource ();
-
- Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
-
- Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
- }
-
- public InputResult? DequeueInput ()
- {
- while (_inputReadyCancellationTokenSource != null
- && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
- {
- _waitForStart.Set ();
- _winChange.Set ();
-
- try
- {
- if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
- {
- if (_inputQueue.Count == 0)
- {
- _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
- }
- }
- }
- catch (OperationCanceledException)
- {
- return null;
- }
- finally
- {
- _inputReady.Reset ();
- }
-
-#if PROCESS_REQUEST
- _neededProcessRequest = false;
-#endif
- if (_inputQueue.Count > 0)
- {
- if (_inputQueue.TryDequeue (out InputResult? result))
- {
- return result;
- }
- }
- }
-
- return null;
- }
-
- private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- // if there is a key available, return it without waiting
- // (or dispatching work to the thread queue)
- if (Console.KeyAvailable)
- {
- return Console.ReadKey (intercept);
- }
-
- if (EscSeqUtils.IncompleteCkInfos is null && EscSeqRequests is { Statuses.Count: > 0 })
- {
- if (_retries > 1)
- {
- if (EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response))
- {
- lock (seqReqStatus!.AnsiRequest._responseLock)
- {
- EscSeqRequests.Statuses.TryDequeue (out _);
-
- seqReqStatus.AnsiRequest.Response = string.Empty;
- seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
- }
- }
-
- _retries = 0;
- }
- else
- {
- _retries++;
- }
- }
- else
- {
- _retries = 0;
- }
-
- if (!_forceRead)
- {
- Task.Delay (100, cancellationToken).Wait (cancellationToken);
- }
- }
-
- cancellationToken.ThrowIfCancellationRequested ();
-
- return default (ConsoleKeyInfo);
- }
-
- internal bool _forceRead;
- private int _retries;
-
- private void ProcessInputQueue ()
- {
- while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
- {
- try
- {
- if (!_forceRead)
- {
- _waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- return;
- }
-
- _waitForStart.Reset ();
-
- if (_inputQueue.Count == 0 || _forceRead)
- {
- ConsoleKey key = 0;
- ConsoleModifiers mod = 0;
- ConsoleKeyInfo newConsoleKeyInfo = default;
-
- while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
- {
- ConsoleKeyInfo consoleKeyInfo;
-
- try
- {
- consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
- }
- catch (OperationCanceledException)
- {
- return;
- }
-
- if (EscSeqUtils.IncompleteCkInfos is { })
- {
- EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
- }
-
- if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
- || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
- {
- if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
- {
- _cki = EscSeqUtils.ResizeArray (
- new ConsoleKeyInfo (
- (char)KeyCode.Esc,
- 0,
- false,
- false,
- false
- ),
- _cki
- );
- }
-
- _isEscSeq = true;
-
- if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
- || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
- || (_cki is { } && char.IsLetter (_cki [^1].KeyChar) && char.IsLower (consoleKeyInfo.KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar))
- || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar)))
- {
- ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
- _cki = null;
- _isEscSeq = false;
-
- ProcessMapConsoleKeyInfo (consoleKeyInfo);
- }
- else
- {
- newConsoleKeyInfo = consoleKeyInfo;
- _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
-
- if (Console.KeyAvailable)
- {
- continue;
- }
-
- ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
- _cki = null;
- _isEscSeq = false;
- }
-
- break;
- }
-
- if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
- {
- ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
- _cki = null;
-
- if (Console.KeyAvailable)
- {
- _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
- }
- else
- {
- ProcessMapConsoleKeyInfo (consoleKeyInfo);
- }
-
- break;
- }
-
- ProcessMapConsoleKeyInfo (consoleKeyInfo);
-
- if (_retries > 0)
- {
- _retries = 0;
- }
-
- break;
- }
- }
-
- _inputReady.Set ();
- }
-
- void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
- {
- _inputQueue.Enqueue (
- new InputResult
- {
- EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
- }
- );
- _isEscSeq = false;
- }
- }
-
- private void CheckWindowSizeChange ()
- {
- void RequestWindowSize (CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- // Wait for a while then check if screen has changed sizes
- Task.Delay (500, cancellationToken).Wait (cancellationToken);
-
- int buffHeight, buffWidth;
-
- if (((NetDriver)_consoleDriver).IsWinPlatform)
- {
- buffHeight = Math.Max (Console.BufferHeight, 0);
- buffWidth = Math.Max (Console.BufferWidth, 0);
- }
- else
- {
- buffHeight = _consoleDriver.Rows;
- buffWidth = _consoleDriver.Cols;
- }
-
- if (EnqueueWindowSizeEvent (
- Math.Max (Console.WindowHeight, 0),
- Math.Max (Console.WindowWidth, 0),
- buffHeight,
- buffWidth
- ))
- {
- return;
- }
- }
-
- cancellationToken.ThrowIfCancellationRequested ();
- }
-
- while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
- {
- try
- {
- _winChange.Wait (_inputReadyCancellationTokenSource.Token);
- _winChange.Reset ();
-
- RequestWindowSize (_inputReadyCancellationTokenSource.Token);
- }
- catch (OperationCanceledException)
- {
- return;
- }
-
- _inputReady.Set ();
- }
- }
-
- /// 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 EscSeqReqStatus seqReqStatus,
- (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
- );
-
- if (isMouse)
- {
- foreach (MouseFlags mf in mouseFlags)
- {
- HandleMouseEvent (MapMouseFlags (mf), pos);
- }
-
- return;
- }
-
- if (seqReqStatus is { })
- {
- //HandleRequestResponseEvent (c1Control, code, values, terminating);
-
- var ckiString = EscSeqUtils.ToString (cki);
-
- lock (seqReqStatus.AnsiRequest._responseLock)
- {
- seqReqStatus.AnsiRequest.Response = ckiString;
- seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, ckiString);
- }
-
- return;
- }
-
- if (!string.IsNullOrEmpty (EscSeqUtils.InvalidRequestTerminator))
- {
- if (EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus result))
- {
- lock (result.AnsiRequest._responseLock)
- {
- result.AnsiRequest.Response = EscSeqUtils.InvalidRequestTerminator;
- result.AnsiRequest.RaiseResponseFromInput (result.AnsiRequest, EscSeqUtils.InvalidRequestTerminator);
-
- EscSeqUtils.InvalidRequestTerminator = null;
- }
- }
-
- return;
- }
-
- if (newConsoleKeyInfo != default)
- {
- HandleKeyboardEvent (newConsoleKeyInfo);
- }
- }
-
- [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
- private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
- {
- MouseButtonState mbs = default;
-
- foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
- {
- if (mouseFlags.HasFlag ((MouseFlags)flag))
- {
- switch (flag)
- {
- case MouseFlags.Button1Pressed:
- mbs |= MouseButtonState.Button1Pressed;
-
- break;
- case MouseFlags.Button1Released:
- mbs |= MouseButtonState.Button1Released;
-
- break;
- case MouseFlags.Button1Clicked:
- mbs |= MouseButtonState.Button1Clicked;
-
- break;
- case MouseFlags.Button1DoubleClicked:
- mbs |= MouseButtonState.Button1DoubleClicked;
-
- break;
- case MouseFlags.Button1TripleClicked:
- mbs |= MouseButtonState.Button1TripleClicked;
-
- break;
- case MouseFlags.Button2Pressed:
- mbs |= MouseButtonState.Button2Pressed;
-
- break;
- case MouseFlags.Button2Released:
- mbs |= MouseButtonState.Button2Released;
-
- break;
- case MouseFlags.Button2Clicked:
- mbs |= MouseButtonState.Button2Clicked;
-
- break;
- case MouseFlags.Button2DoubleClicked:
- mbs |= MouseButtonState.Button2DoubleClicked;
-
- break;
- case MouseFlags.Button2TripleClicked:
- mbs |= MouseButtonState.Button2TripleClicked;
-
- break;
- case MouseFlags.Button3Pressed:
- mbs |= MouseButtonState.Button3Pressed;
-
- break;
- case MouseFlags.Button3Released:
- mbs |= MouseButtonState.Button3Released;
-
- break;
- case MouseFlags.Button3Clicked:
- mbs |= MouseButtonState.Button3Clicked;
-
- break;
- case MouseFlags.Button3DoubleClicked:
- mbs |= MouseButtonState.Button3DoubleClicked;
-
- break;
- case MouseFlags.Button3TripleClicked:
- mbs |= MouseButtonState.Button3TripleClicked;
-
- break;
- case MouseFlags.WheeledUp:
- mbs |= MouseButtonState.ButtonWheeledUp;
-
- break;
- case MouseFlags.WheeledDown:
- mbs |= MouseButtonState.ButtonWheeledDown;
-
- break;
- case MouseFlags.WheeledLeft:
- mbs |= MouseButtonState.ButtonWheeledLeft;
-
- break;
- case MouseFlags.WheeledRight:
- mbs |= MouseButtonState.ButtonWheeledRight;
-
- break;
- case MouseFlags.Button4Pressed:
- mbs |= MouseButtonState.Button4Pressed;
-
- break;
- case MouseFlags.Button4Released:
- mbs |= MouseButtonState.Button4Released;
-
- break;
- case MouseFlags.Button4Clicked:
- mbs |= MouseButtonState.Button4Clicked;
-
- break;
- case MouseFlags.Button4DoubleClicked:
- mbs |= MouseButtonState.Button4DoubleClicked;
-
- break;
- case MouseFlags.Button4TripleClicked:
- mbs |= MouseButtonState.Button4TripleClicked;
-
- break;
- case MouseFlags.ButtonShift:
- mbs |= MouseButtonState.ButtonShift;
-
- break;
- case MouseFlags.ButtonCtrl:
- mbs |= MouseButtonState.ButtonCtrl;
-
- break;
- case MouseFlags.ButtonAlt:
- mbs |= MouseButtonState.ButtonAlt;
-
- break;
- case MouseFlags.ReportMousePosition:
- mbs |= MouseButtonState.ReportMousePosition;
-
- break;
- case MouseFlags.AllEvents:
- mbs |= MouseButtonState.AllEvents;
-
- break;
- }
- }
- }
-
- return mbs;
- }
-
- private Point _lastCursorPosition;
-
- //private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
- //{
- // if (terminating ==
-
- // // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
- // // The observation is correct because the response isn't immediate and this is useless
- // EscSeqUtils.CSI_RequestCursorPositionReport.Terminator)
- // {
- // var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
-
- // if (_lastCursorPosition.Y != point.Y)
- // {
- // _lastCursorPosition = point;
- // var eventType = EventType.WindowPosition;
- // var winPositionEv = new WindowPositionEvent { CursorPosition = point };
-
- // _inputQueue.Enqueue (
- // new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
- // );
- // }
- // else
- // {
- // return;
- // }
- // }
- // else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator)
- // {
- // if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value)
- // {
- // EnqueueWindowSizeEvent (
- // Math.Max (int.Parse (values [1]), 0),
- // Math.Max (int.Parse (values [2]), 0),
- // Math.Max (int.Parse (values [1]), 0),
- // Math.Max (int.Parse (values [2]), 0)
- // );
- // }
- // else
- // {
- // EnqueueRequestResponseEvent (c1Control, code, values, terminating);
- // }
- // }
- // else
- // {
- // EnqueueRequestResponseEvent (c1Control, code, values, terminating);
- // }
-
- // _inputReady.Set ();
- //}
-
- //private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
- //{
- // var eventType = EventType.RequestResponse;
- // var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
-
- // _inputQueue.Enqueue (
- // new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
- // );
- //}
-
- private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
- {
- var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
-
- _inputQueue.Enqueue (
- new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
- );
-
- _inputReady.Set ();
- }
-
- public enum EventType
- {
- Key = 1,
- Mouse = 2,
- WindowSize = 3,
- WindowPosition = 4,
- RequestResponse = 5
- }
-
- [Flags]
- public enum MouseButtonState
- {
- Button1Pressed = 0x1,
- Button1Released = 0x2,
- Button1Clicked = 0x4,
- Button1DoubleClicked = 0x8,
- Button1TripleClicked = 0x10,
- Button2Pressed = 0x20,
- Button2Released = 0x40,
- Button2Clicked = 0x80,
- Button2DoubleClicked = 0x100,
- Button2TripleClicked = 0x200,
- Button3Pressed = 0x400,
- Button3Released = 0x800,
- Button3Clicked = 0x1000,
- Button3DoubleClicked = 0x2000,
- Button3TripleClicked = 0x4000,
- ButtonWheeledUp = 0x8000,
- ButtonWheeledDown = 0x10000,
- ButtonWheeledLeft = 0x20000,
- ButtonWheeledRight = 0x40000,
- Button4Pressed = 0x80000,
- Button4Released = 0x100000,
- Button4Clicked = 0x200000,
- Button4DoubleClicked = 0x400000,
- Button4TripleClicked = 0x800000,
- ButtonShift = 0x1000000,
- ButtonCtrl = 0x2000000,
- ButtonAlt = 0x4000000,
- ReportMousePosition = 0x8000000,
- AllEvents = -1
- }
-
- public struct MouseEvent
- {
- public Point Position;
- public MouseButtonState ButtonState;
- }
-
- public struct WindowSizeEvent
- {
- public Size Size;
- }
-
- public struct WindowPositionEvent
- {
- public int Top;
- public int Left;
- public Point CursorPosition;
- }
-
- public struct RequestResponseEvent
- {
- public (string c1Control, string code, string [] values, string terminating) ResultTuple;
- }
-
- public struct InputResult
- {
- public EventType EventType;
- public ConsoleKeyInfo ConsoleKeyInfo;
- public MouseEvent MouseEvent;
- public WindowSizeEvent WindowSizeEvent;
- public WindowPositionEvent WindowPositionEvent;
- public RequestResponseEvent RequestResponseEvent;
-
- public readonly override string ToString ()
- {
- return EventType switch
- {
- EventType.Key => ToString (ConsoleKeyInfo),
- EventType.Mouse => MouseEvent.ToString (),
-
- //EventType.WindowSize => WindowSize.ToString (),
- //EventType.RequestResponse => RequestResponse.ToString (),
- _ => "Unknown event type: " + EventType
- };
- }
-
- /// 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;
- internal 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 Refresh ()
- {
- UpdateScreen ();
- UpdateCursor ();
- }
-
- 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 void UpdateScreen ()
- {
- if (RunningUnitTests
- || _winSizeChanging
- || Console.WindowHeight < 1
- || Contents.Length != Rows * Cols
- || Rows != Console.WindowHeight)
- {
- return;
- }
-
- var top = 0;
- var left = 0;
- int rows = Rows;
- int cols = Cols;
- var output = new StringBuilder ();
- Attribute? redrawAttr = null;
- int lastCol = -1;
-
- CursorVisibility? savedVisibility = _cachedCursorVisibility;
- SetCursorVisibility (CursorVisibility.Invisible);
-
- for (int row = top; row < rows; row++)
- {
- if (Console.WindowHeight < 1)
- {
- return;
- }
-
- if (!_dirtyLines [row])
- {
- continue;
- }
-
- if (!SetCursorPosition (0, row))
- {
- return;
- }
-
- _dirtyLines [row] = false;
- output.Clear ();
-
- for (int col = left; col < cols; col++)
- {
- lastCol = -1;
- var outputWidth = 0;
-
- for (; col < cols; col++)
- {
- if (!Contents [row, col].IsDirty)
- {
- if (output.Length > 0)
- {
- WriteToConsole (output, ref lastCol, row, ref outputWidth);
- }
- else if (lastCol == -1)
- {
- lastCol = col;
- }
-
- if (lastCol + 1 < cols)
- {
- lastCol++;
- }
-
- continue;
- }
-
- if (lastCol == -1)
- {
- lastCol = col;
- }
-
- Attribute attr = Contents [row, col].Attribute.Value;
-
- // Performance: Only send the escape sequence if the attribute has changed.
- if (attr != redrawAttr)
- {
- redrawAttr = attr;
-
- if (Force16Colors)
- {
- output.Append (
- EscSeqUtils.CSI_SetGraphicsRendition (
- MapColors (
- (ConsoleColor)attr.Background.GetClosestNamedColor16 (),
- false
- ),
- MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ())
- )
- );
- }
- else
- {
- output.Append (
- EscSeqUtils.CSI_SetForegroundColorRGB (
- attr.Foreground.R,
- attr.Foreground.G,
- attr.Foreground.B
- )
- );
-
- output.Append (
- EscSeqUtils.CSI_SetBackgroundColorRGB (
- attr.Background.R,
- attr.Background.G,
- attr.Background.B
- )
- );
- }
- }
-
- outputWidth++;
- Rune rune = Contents [row, col].Rune;
- output.Append (rune);
-
- if (Contents [row, col].CombiningMarks.Count > 0)
- {
- // AtlasEngine does not support NON-NORMALIZED combining marks in a way
- // compatible with the driver architecture. Any CMs (except in the first col)
- // are correctly combined with the base char, but are ALSO treated as 1 column
- // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
- //
- // For now, we just ignore the list of CMs.
- //foreach (var combMark in Contents [row, col].CombiningMarks) {
- // output.Append (combMark);
- //}
- // WriteToConsole (output, ref lastCol, row, ref outputWidth);
- }
- else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
- {
- WriteToConsole (output, ref lastCol, row, ref outputWidth);
- SetCursorPosition (col - 1, row);
- }
-
- Contents [row, col].IsDirty = false;
- }
- }
-
- if (output.Length > 0)
- {
- SetCursorPosition (lastCol, row);
- Console.Write (output);
- }
-
- foreach (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;
- }
- }
-
- internal override void End ()
- {
- if (IsWinPlatform)
- {
- NetWinConsole?.Cleanup ();
- }
-
- StopReportingMouseMoves ();
-
- _ansiResponseTokenSource?.Cancel ();
- _ansiResponseTokenSource?.Dispose ();
-
- _waitAnsiResponse?.Dispose ();
-
- if (!RunningUnitTests)
- {
- Console.ResetColor ();
-
- //Disable alternative screen buffer.
- Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-
- //Set cursor key to cursor.
- Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
- Console.Out.Close ();
- }
- }
-
- 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 (0, 0, Cols, Rows);
- }
- catch (ArgumentOutOfRangeException)
- {
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (0, 0, Cols, Rows);
- }
- }
- else
- {
- Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
- }
-
- // CONCURRENCY: Unsynchronized access to Clip is not safe.
- Clip = new (0, 0, Cols, Rows);
- }
-
- #endregion
-
- #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 readonly ManualResetEventSlim _waitAnsiResponse = new (false);
- private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
-
- ///
- public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
- {
- if (_mainLoopDriver is null)
- {
- return string.Empty;
- }
-
- try
- {
- lock (ansiRequest._responseLock)
- {
- ansiRequest.ResponseFromInput += (s, e) =>
- {
- Debug.Assert (s == ansiRequest);
- Debug.Assert (e == ansiRequest.Response);
-
- _waitAnsiResponse.Set ();
- };
-
- _mainLoopDriver._netEvents.EscSeqRequests.Add (ansiRequest);
-
- _mainLoopDriver._netEvents._forceRead = true;
- }
-
- if (!_ansiResponseTokenSource.IsCancellationRequested)
- {
- _mainLoopDriver._netEvents._waitForStart.Set ();
-
- if (!_mainLoopDriver._waitForProbe.IsSet)
- {
- _mainLoopDriver._waitForProbe.Set ();
- }
-
- _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- return string.Empty;
- }
-
- lock (ansiRequest._responseLock)
- {
- _mainLoopDriver._netEvents._forceRead = false;
-
- if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
- {
- if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.Count > 0
- && string.IsNullOrEmpty (request.AnsiRequest.Response))
- {
- lock (request!.AnsiRequest._responseLock)
- {
- // Bad request or no response at all
- _mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryDequeue (out _);
- }
- }
- }
-
- _waitAnsiResponse.Reset ();
-
- return ansiRequest.Response;
- }
- }
-
- ///
- public override void WriteRaw (string ansi) { throw new NotImplementedException (); }
-
- 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 ConcurrentQueue _resultQueue = new ();
- internal 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;
-
- if (ConsoleDriver.RunningUnitTests)
- {
- return;
- }
-
- Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
- }
-
- void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
-
- bool IMainLoopDriver.EventsPending ()
- {
- _waitForProbe.Set ();
-
- if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
- {
- return true;
- }
-
- try
- {
- if (!_eventReadyTokenSource.IsCancellationRequested)
- {
- // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
- // are no timers, but there IS an idle handler waiting.
- _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- return true;
- }
- finally
- {
- _eventReady.Reset ();
- }
-
- _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
-
- if (!_eventReadyTokenSource.IsCancellationRequested)
- {
- return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
- }
-
- return true;
- }
-
- void IMainLoopDriver.Iteration ()
- {
- while (_resultQueue.Count > 0)
- {
- // Always dequeue even if it's null and invoke if isn't null
- if (_resultQueue.TryDequeue (out InputResult? dequeueResult))
- {
- if (dequeueResult is { })
- {
- ProcessInput?.Invoke (dequeueResult.Value);
- }
- }
- }
- }
-
- void IMainLoopDriver.TearDown ()
- {
- _inputHandlerTokenSource?.Cancel ();
- _inputHandlerTokenSource?.Dispose ();
- _eventReadyTokenSource?.Cancel ();
- _eventReadyTokenSource?.Dispose ();
-
- _eventReady?.Dispose ();
-
- _resultQueue?.Clear ();
- _waitForProbe?.Dispose ();
- _netEvents?.Dispose ();
- _netEvents = null;
-
- _mainLoop = null;
- }
-
- private void NetInputHandler ()
- {
- while (_mainLoop is { })
- {
- try
- {
- if (!_netEvents._forceRead && !_inputHandlerTokenSource.IsCancellationRequested)
- {
- _waitForProbe.Wait (_inputHandlerTokenSource.Token);
- }
- }
- catch (OperationCanceledException)
- {
- return;
- }
- finally
- {
- if (_waitForProbe.IsSet)
- {
- _waitForProbe.Reset ();
- }
- }
-
- if (_inputHandlerTokenSource.IsCancellationRequested)
- {
- return;
- }
-
- _inputHandlerTokenSource.Token.ThrowIfCancellationRequested ();
-
- if (_resultQueue.Count == 0)
- {
- _resultQueue.Enqueue (_netEvents.DequeueInput ());
- }
-
- try
- {
- while (_resultQueue.Count > 0 && _resultQueue.TryPeek (out InputResult? result) && result is null)
- {
- // Dequeue null values
- _resultQueue.TryDequeue (out _);
- }
- }
- catch (InvalidOperationException) // Peek can raise an exception
- { }
-
- 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 000000000..e4103d8c3
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs
@@ -0,0 +1,965 @@
+// TODO: #nullable enable
+//
+// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
+//
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
+using static Terminal.Gui.NetEvents;
+
+namespace Terminal.Gui;
+
+internal class NetDriver : ConsoleDriver
+{
+ public bool IsWinPlatform { get; private set; }
+ public NetWinVTConsole NetWinConsole { get; private set; }
+
+ public override void Refresh ()
+ {
+ UpdateScreen ();
+ UpdateCursor ();
+ }
+
+ public override void Suspend ()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Unix)
+ {
+ return;
+ }
+
+ StopReportingMouseMoves ();
+
+ if (!RunningUnitTests)
+ {
+ Console.ResetColor ();
+ Console.Clear ();
+
+ //Disable alternative screen buffer.
+ Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+ //Set cursor key to cursor.
+ Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+
+ Platform.Suspend ();
+
+ //Enable alternative screen buffer.
+ Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+ SetContentsAsDirty ();
+ Refresh ();
+ }
+
+ StartReportingMouseMoves ();
+ }
+
+ #region Screen and Contents
+
+ public override void UpdateScreen ()
+ {
+ if (RunningUnitTests
+ || _winSizeChanging
+ || Console.WindowHeight < 1
+ || Contents.Length != Rows * Cols
+ || Rows != Console.WindowHeight)
+ {
+ return;
+ }
+
+ var top = 0;
+ var left = 0;
+ int rows = Rows;
+ int cols = Cols;
+ var output = new StringBuilder ();
+ Attribute? redrawAttr = null;
+ int lastCol = -1;
+
+ CursorVisibility? savedVisibility = _cachedCursorVisibility;
+ SetCursorVisibility (CursorVisibility.Invisible);
+
+ for (int row = top; row < rows; row++)
+ {
+ if (Console.WindowHeight < 1)
+ {
+ return;
+ }
+
+ if (!_dirtyLines [row])
+ {
+ continue;
+ }
+
+ if (!SetCursorPosition (0, row))
+ {
+ return;
+ }
+
+ _dirtyLines [row] = false;
+ output.Clear ();
+
+ for (int col = left; col < cols; col++)
+ {
+ lastCol = -1;
+ var outputWidth = 0;
+
+ for (; col < cols; col++)
+ {
+ if (!Contents [row, col].IsDirty)
+ {
+ if (output.Length > 0)
+ {
+ WriteToConsole (output, ref lastCol, row, ref outputWidth);
+ }
+ else if (lastCol == -1)
+ {
+ lastCol = col;
+ }
+
+ if (lastCol + 1 < cols)
+ {
+ lastCol++;
+ }
+
+ continue;
+ }
+
+ if (lastCol == -1)
+ {
+ lastCol = col;
+ }
+
+ Attribute attr = Contents [row, col].Attribute.Value;
+
+ // Performance: Only send the escape sequence if the attribute has changed.
+ if (attr != redrawAttr)
+ {
+ redrawAttr = attr;
+
+ if (Force16Colors)
+ {
+ output.Append (
+ EscSeqUtils.CSI_SetGraphicsRendition (
+ MapColors (
+ (ConsoleColor)attr.Background.GetClosestNamedColor16 (),
+ false
+ ),
+ MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ())
+ )
+ );
+ }
+ else
+ {
+ output.Append (
+ EscSeqUtils.CSI_SetForegroundColorRGB (
+ attr.Foreground.R,
+ attr.Foreground.G,
+ attr.Foreground.B
+ )
+ );
+
+ output.Append (
+ EscSeqUtils.CSI_SetBackgroundColorRGB (
+ attr.Background.R,
+ attr.Background.G,
+ attr.Background.B
+ )
+ );
+ }
+ }
+
+ outputWidth++;
+ Rune rune = Contents [row, col].Rune;
+ output.Append (rune);
+
+ if (Contents [row, col].CombiningMarks.Count > 0)
+ {
+ // AtlasEngine does not support NON-NORMALIZED combining marks in a way
+ // compatible with the driver architecture. Any CMs (except in the first col)
+ // are correctly combined with the base char, but are ALSO treated as 1 column
+ // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
+ //
+ // For now, we just ignore the list of CMs.
+ //foreach (var combMark in Contents [row, col].CombiningMarks) {
+ // output.Append (combMark);
+ //}
+ // WriteToConsole (output, ref lastCol, row, ref outputWidth);
+ }
+ else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
+ {
+ WriteToConsole (output, ref lastCol, row, ref outputWidth);
+ SetCursorPosition (col - 1, row);
+ }
+
+ Contents [row, col].IsDirty = false;
+ }
+ }
+
+ if (output.Length > 0)
+ {
+ SetCursorPosition (lastCol, row);
+ Console.Write (output);
+ }
+
+ foreach (SixelToRender s in Application.Sixel)
+ {
+ if (!string.IsNullOrWhiteSpace (s.SixelData))
+ {
+ SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
+ Console.Write (s.SixelData);
+ }
+ }
+ }
+
+ SetCursorPosition (0, 0);
+
+ _cachedCursorVisibility = savedVisibility;
+
+ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
+ {
+ SetCursorPosition (lastCol, row);
+ Console.Write (output);
+ output.Clear ();
+ lastCol += outputWidth;
+ outputWidth = 0;
+ }
+ }
+
+ #endregion Screen and Contents
+
+ #region Init/End/MainLoop
+
+ internal NetMainLoop _mainLoopDriver;
+
+ internal override MainLoop Init ()
+ {
+ PlatformID p = Environment.OSVersion.Platform;
+
+ if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+ {
+ IsWinPlatform = true;
+
+ try
+ {
+ NetWinConsole = new ();
+ }
+ catch (ApplicationException)
+ {
+ // Likely running as a unit test, or in a non-interactive session.
+ }
+ }
+
+ if (IsWinPlatform)
+ {
+ Clipboard = new WindowsClipboard ();
+ }
+ else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
+ {
+ Clipboard = new MacOSXClipboard ();
+ }
+ else
+ {
+ if (CursesDriver.Is_WSL_Platform ())
+ {
+ Clipboard = new WSLClipboard ();
+ }
+ else
+ {
+ Clipboard = new CursesClipboard ();
+ }
+ }
+
+ if (!RunningUnitTests)
+ {
+ Console.TreatControlCAsInput = true;
+
+ Cols = Console.WindowWidth;
+ Rows = Console.WindowHeight;
+
+ //Enable alternative screen buffer.
+ Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+ //Set cursor key to application.
+ Console.Out.Write (EscSeqUtils.CSI_HideCursor);
+ }
+ else
+ {
+ // We are being run in an environment that does not support a console
+ // such as a unit test, or a pipe.
+ Cols = 80;
+ Rows = 24;
+ }
+
+ ResizeScreen ();
+ ClearContents ();
+ CurrentAttribute = new (Color.White, Color.Black);
+
+ StartReportingMouseMoves ();
+
+ _mainLoopDriver = new (this);
+ _mainLoopDriver.ProcessInput = ProcessInput;
+
+ return new (_mainLoopDriver);
+ }
+
+ private void ProcessInput (InputResult inputEvent)
+ {
+ switch (inputEvent.EventType)
+ {
+ case EventType.Key:
+ ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
+
+ //if (consoleKeyInfo.Key == ConsoleKey.Packet) {
+ // consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+ //}
+
+ //Debug.WriteLine ($"event: {inputEvent}");
+
+ KeyCode map = MapKey (consoleKeyInfo);
+
+ if (map == KeyCode.Null)
+ {
+ break;
+ }
+
+ OnKeyDown (new (map));
+ OnKeyUp (new (map));
+
+ break;
+ case EventType.Mouse:
+ MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
+
+ //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
+ OnMouseEvent (me);
+
+ break;
+ case EventType.WindowSize:
+ _winSizeChanging = true;
+ Top = 0;
+ Left = 0;
+ Cols = inputEvent.WindowSizeEvent.Size.Width;
+ Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
+ ;
+ ResizeScreen ();
+ ClearContents ();
+ _winSizeChanging = false;
+ OnSizeChanged (new (new (Cols, Rows)));
+
+ break;
+ case EventType.RequestResponse:
+ break;
+ case EventType.WindowPosition:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException ();
+ }
+ }
+
+ internal override void End ()
+ {
+ if (IsWinPlatform)
+ {
+ NetWinConsole?.Cleanup ();
+ }
+
+ StopReportingMouseMoves ();
+
+ _ansiResponseTokenSource?.Cancel ();
+ _ansiResponseTokenSource?.Dispose ();
+
+ _waitAnsiResponse?.Dispose ();
+
+ if (!RunningUnitTests)
+ {
+ Console.ResetColor ();
+
+ //Disable alternative screen buffer.
+ Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+ //Set cursor key to cursor.
+ Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+ Console.Out.Close ();
+ }
+ }
+
+ #endregion Init/End/MainLoop
+
+ #region Color Handling
+
+ public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
+ || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
+
+ private const int COLOR_BLACK = 30;
+ private const int COLOR_BLUE = 34;
+ private const int COLOR_BRIGHT_BLACK = 90;
+ private const int COLOR_BRIGHT_BLUE = 94;
+ private const int COLOR_BRIGHT_CYAN = 96;
+ private const int COLOR_BRIGHT_GREEN = 92;
+ private const int COLOR_BRIGHT_MAGENTA = 95;
+ private const int COLOR_BRIGHT_RED = 91;
+ private const int COLOR_BRIGHT_WHITE = 97;
+ private const int COLOR_BRIGHT_YELLOW = 93;
+ private const int COLOR_CYAN = 36;
+ private const int COLOR_GREEN = 32;
+ private const int COLOR_MAGENTA = 35;
+ private const int COLOR_RED = 31;
+ private const int COLOR_WHITE = 37;
+ private const int COLOR_YELLOW = 33;
+
+ // Cache the list of ConsoleColor values.
+ [UnconditionalSuppressMessage (
+ "AOT",
+ "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
+ Justification = "")]
+ 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 (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
+
+ return true;
+ }
+
+ private CursorVisibility? _cachedCursorVisibility;
+
+ public override void UpdateCursor ()
+ {
+ EnsureCursorVisibility ();
+
+ if (Col >= 0 && Col < Cols && Row >= 0 && Row <= Rows)
+ {
+ SetCursorPosition (Col, Row);
+ SetWindowPosition (0, Row);
+ }
+ }
+
+ public override bool GetCursorVisibility (out CursorVisibility visibility)
+ {
+ visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
+
+ return visibility == CursorVisibility.Default;
+ }
+
+ public override bool SetCursorVisibility (CursorVisibility visibility)
+ {
+ _cachedCursorVisibility = visibility;
+
+ Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+
+ return visibility == CursorVisibility.Default;
+ }
+
+ public override bool EnsureCursorVisibility ()
+ {
+ if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+ {
+ GetCursorVisibility (out CursorVisibility cursorVisibility);
+ _cachedCursorVisibility = cursorVisibility;
+ SetCursorVisibility (CursorVisibility.Invisible);
+
+ return false;
+ }
+
+ SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
+
+ return _cachedCursorVisibility == CursorVisibility.Default;
+ }
+
+ #endregion
+
+ #region Mouse Handling
+
+ public void StartReportingMouseMoves ()
+ {
+ if (!RunningUnitTests)
+ {
+ Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+ }
+ }
+
+ public void StopReportingMouseMoves ()
+ {
+ if (!RunningUnitTests)
+ {
+ Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+ }
+ }
+
+ private MouseEventArgs ToDriverMouse (MouseEvent me)
+ {
+ //System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}");
+
+ MouseFlags mouseFlag = 0;
+
+ if ((me.ButtonState & MouseButtonState.Button1Pressed) != 0)
+ {
+ mouseFlag |= MouseFlags.Button1Pressed;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button1Released) != 0)
+ {
+ mouseFlag |= MouseFlags.Button1Released;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button1Clicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button1DoubleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button1TripleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0)
+ {
+ mouseFlag |= MouseFlags.Button2Pressed;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button2Released) != 0)
+ {
+ mouseFlag |= MouseFlags.Button2Released;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button2Clicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button2DoubleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button2TripleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0)
+ {
+ mouseFlag |= MouseFlags.Button3Pressed;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button3Released) != 0)
+ {
+ mouseFlag |= MouseFlags.Button3Released;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button3Clicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button3DoubleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button3TripleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0)
+ {
+ mouseFlag |= MouseFlags.WheeledUp;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0)
+ {
+ mouseFlag |= MouseFlags.WheeledDown;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0)
+ {
+ mouseFlag |= MouseFlags.WheeledLeft;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0)
+ {
+ mouseFlag |= MouseFlags.WheeledRight;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0)
+ {
+ mouseFlag |= MouseFlags.Button4Pressed;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button4Released) != 0)
+ {
+ mouseFlag |= MouseFlags.Button4Released;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button4Clicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button4DoubleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0)
+ {
+ mouseFlag |= MouseFlags.Button4TripleClicked;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0)
+ {
+ mouseFlag |= MouseFlags.ReportMousePosition;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonShift) != 0)
+ {
+ mouseFlag |= MouseFlags.ButtonShift;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0)
+ {
+ mouseFlag |= MouseFlags.ButtonCtrl;
+ }
+
+ if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0)
+ {
+ mouseFlag |= MouseFlags.ButtonAlt;
+ }
+
+ return new() { Position = me.Position, Flags = mouseFlag };
+ }
+
+ #endregion Mouse Handling
+
+ #region Keyboard Handling
+
+ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+ {
+ var input = new InputResult
+ {
+ EventType = EventType.Key, ConsoleKeyInfo = new (keyChar, key, shift, alt, control)
+ };
+
+ try
+ {
+ ProcessInput (input);
+ }
+ catch (OverflowException)
+ { }
+ }
+
+ private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+ {
+ if (consoleKeyInfo.Key != ConsoleKey.Packet)
+ {
+ return consoleKeyInfo;
+ }
+
+ ConsoleModifiers mod = consoleKeyInfo.Modifiers;
+ bool shift = (mod & ConsoleModifiers.Shift) != 0;
+ bool alt = (mod & ConsoleModifiers.Alt) != 0;
+ bool control = (mod & ConsoleModifiers.Control) != 0;
+
+ ConsoleKeyInfo cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
+
+ return new (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
+ }
+
+ private KeyCode MapKey (ConsoleKeyInfo keyInfo)
+ {
+ switch (keyInfo.Key)
+ {
+ case ConsoleKey.OemPeriod:
+ case ConsoleKey.OemComma:
+ case ConsoleKey.OemPlus:
+ case ConsoleKey.OemMinus:
+ case ConsoleKey.Packet:
+ case ConsoleKey.Oem1:
+ case ConsoleKey.Oem2:
+ case ConsoleKey.Oem3:
+ case ConsoleKey.Oem4:
+ case ConsoleKey.Oem5:
+ case ConsoleKey.Oem6:
+ case ConsoleKey.Oem7:
+ case ConsoleKey.Oem8:
+ case ConsoleKey.Oem102:
+ if (keyInfo.KeyChar == 0)
+ {
+ // If the keyChar is 0, keyInfo.Key value is not a printable character.
+
+ return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
+ }
+
+ if (keyInfo.Modifiers != ConsoleModifiers.Shift)
+ {
+ // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+ }
+
+ // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
+ // and passing on Shift would be redundant.
+ return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
+ }
+
+ // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
+ if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
+ {
+ if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
+ {
+ return KeyCode.Tab;
+ }
+
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key);
+ }
+
+ // Handle control keys (e.g. CursorUp)
+ if (keyInfo.Key != ConsoleKey.None
+ && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
+ {
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
+ }
+
+ if ((ConsoleKey)keyInfo.KeyChar is >= ConsoleKey.A and <= ConsoleKey.Z)
+ {
+ // Shifted
+ keyInfo = new (
+ keyInfo.KeyChar,
+ (ConsoleKey)keyInfo.KeyChar,
+ true,
+ keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
+ keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
+ }
+
+ if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z)
+ {
+ // Unshifted
+ keyInfo = new (
+ keyInfo.KeyChar,
+ (ConsoleKey)(keyInfo.KeyChar - 32),
+ false,
+ keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
+ keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
+ }
+
+ if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z)
+ {
+ if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
+ || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
+ {
+ // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos
+ return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key);
+ }
+
+ if (keyInfo.Modifiers == ConsoleModifiers.Shift)
+ {
+ // If ShiftMask is on add the ShiftMask
+ if (char.IsUpper (keyInfo.KeyChar))
+ {
+ return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
+ }
+ }
+
+ return (KeyCode)keyInfo.Key;
+ }
+
+ return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
+ }
+
+ #endregion Keyboard Handling
+
+ #region Low-Level DotNet tuff
+
+ private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
+ private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
+
+ ///
+ public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
+ {
+ if (_mainLoopDriver is null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ lock (ansiRequest._responseLock)
+ {
+ ansiRequest.ResponseFromInput += (s, e) =>
+ {
+ Debug.Assert (s == ansiRequest);
+ Debug.Assert (e == ansiRequest.Response);
+
+ _waitAnsiResponse.Set ();
+ };
+
+ _mainLoopDriver._netEvents.EscSeqRequests.Add (ansiRequest);
+
+ _mainLoopDriver._netEvents._forceRead = true;
+ }
+
+ if (!_ansiResponseTokenSource.IsCancellationRequested)
+ {
+ _mainLoopDriver._netEvents._waitForStart.Set ();
+
+ if (!_mainLoopDriver._waitForProbe.IsSet)
+ {
+ _mainLoopDriver._waitForProbe.Set ();
+ }
+
+ _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return string.Empty;
+ }
+
+ lock (ansiRequest._responseLock)
+ {
+ _mainLoopDriver._netEvents._forceRead = false;
+
+ if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
+ {
+ if (_mainLoopDriver._netEvents.EscSeqRequests.Statuses.Count > 0
+ && string.IsNullOrEmpty (request.AnsiRequest.Response))
+ {
+ lock (request!.AnsiRequest._responseLock)
+ {
+ // Bad request or no response at all
+ _mainLoopDriver._netEvents.EscSeqRequests.Statuses.TryDequeue (out _);
+ }
+ }
+ }
+
+ _waitAnsiResponse.Reset ();
+
+ return ansiRequest.Response;
+ }
+ }
+
+ ///
+ public override void WriteRaw (string ansi) { throw new NotImplementedException (); }
+
+ private volatile bool _winSizeChanging;
+
+ private void SetWindowPosition (int col, int row)
+ {
+ if (!RunningUnitTests)
+ {
+ Top = Console.WindowTop;
+ Left = Console.WindowLeft;
+ }
+ else
+ {
+ Top = row;
+ Left = col;
+ }
+ }
+
+ private void ResizeScreen ()
+ {
+ // Not supported on Unix.
+ if (IsWinPlatform)
+ {
+ // Can raise an exception while is still resizing.
+ try
+ {
+#pragma warning disable CA1416
+ if (Console.WindowHeight > 0)
+ {
+ Console.CursorTop = 0;
+ Console.CursorLeft = 0;
+ Console.WindowTop = 0;
+ Console.WindowLeft = 0;
+
+ if (Console.WindowHeight > Rows)
+ {
+ Console.SetWindowSize (Cols, Rows);
+ }
+
+ Console.SetBufferSize (Cols, Rows);
+ }
+#pragma warning restore CA1416
+ }
+
+ // INTENT: Why are these eating the exceptions?
+ // Comments would be good here.
+ catch (IOException)
+ {
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (0, 0, Cols, Rows);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (0, 0, Cols, Rows);
+ }
+ }
+ else
+ {
+ Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
+ }
+
+ // CONCURRENCY: Unsynchronized access to Clip is not safe.
+ Clip = new (0, 0, Cols, Rows);
+ }
+
+ #endregion Low-Level DotNet tuff
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
new file mode 100644
index 000000000..939ed0d2d
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
@@ -0,0 +1,753 @@
+// TODO: #nullable enable
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+internal class NetEvents : IDisposable
+{
+ private readonly ManualResetEventSlim _inputReady = new (false);
+ private CancellationTokenSource _inputReadyCancellationTokenSource;
+ internal readonly ManualResetEventSlim _waitForStart = new (false);
+
+ //CancellationTokenSource _waitForStartCancellationTokenSource;
+ private readonly ManualResetEventSlim _winChange = new (false);
+ private readonly ConcurrentQueue _inputQueue = new ();
+ private readonly ConsoleDriver _consoleDriver;
+ private ConsoleKeyInfo [] _cki;
+ private bool _isEscSeq;
+#if PROCESS_REQUEST
+ bool _neededProcessRequest;
+#endif
+ public EscSeqRequests EscSeqRequests { get; } = new ();
+
+ public NetEvents (ConsoleDriver consoleDriver)
+ {
+ _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
+ _inputReadyCancellationTokenSource = new CancellationTokenSource ();
+
+ Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
+
+ Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
+ }
+
+ public InputResult? DequeueInput ()
+ {
+ while (_inputReadyCancellationTokenSource != null
+ && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+ {
+ _waitForStart.Set ();
+ _winChange.Set ();
+
+ try
+ {
+ if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+ {
+ if (_inputQueue.Count == 0)
+ {
+ _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return null;
+ }
+ finally
+ {
+ _inputReady.Reset ();
+ }
+
+#if PROCESS_REQUEST
+ _neededProcessRequest = false;
+#endif
+ if (_inputQueue.Count > 0)
+ {
+ if (_inputQueue.TryDequeue (out InputResult? result))
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ // if there is a key available, return it without waiting
+ // (or dispatching work to the thread queue)
+ if (Console.KeyAvailable)
+ {
+ return Console.ReadKey (intercept);
+ }
+
+ if (EscSeqUtils.IncompleteCkInfos is null && EscSeqRequests is { Statuses.Count: > 0 })
+ {
+ if (_retries > 1)
+ {
+ if (EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response))
+ {
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ EscSeqRequests.Statuses.TryDequeue (out _);
+
+ seqReqStatus.AnsiRequest.Response = string.Empty;
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
+ }
+ }
+
+ _retries = 0;
+ }
+ else
+ {
+ _retries++;
+ }
+ }
+ else
+ {
+ _retries = 0;
+ }
+
+ if (!_forceRead)
+ {
+ Task.Delay (100, cancellationToken).Wait (cancellationToken);
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested ();
+
+ return default (ConsoleKeyInfo);
+ }
+
+ internal bool _forceRead;
+ private int _retries;
+
+ private void ProcessInputQueue ()
+ {
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ try
+ {
+ if (!_forceRead)
+ {
+ _waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ _waitForStart.Reset ();
+
+ if (_inputQueue.Count == 0 || _forceRead)
+ {
+ ConsoleKey key = 0;
+ ConsoleModifiers mod = 0;
+ ConsoleKeyInfo newConsoleKeyInfo = default;
+
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ ConsoleKeyInfo consoleKeyInfo;
+
+ try
+ {
+ consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ if (EscSeqUtils.IncompleteCkInfos is { })
+ {
+ EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
+ }
+
+ if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
+ || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
+ {
+ if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
+ {
+ _cki = EscSeqUtils.ResizeArray (
+ new ConsoleKeyInfo (
+ (char)KeyCode.Esc,
+ 0,
+ false,
+ false,
+ false
+ ),
+ _cki
+ );
+ }
+
+ _isEscSeq = true;
+
+ if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
+ || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
+ || (_cki is { } && char.IsLetter (_cki [^1].KeyChar) && char.IsLower (consoleKeyInfo.KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar))
+ || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetter (consoleKeyInfo.KeyChar)))
+ {
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+ _cki = null;
+ _isEscSeq = false;
+
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+ }
+ else
+ {
+ newConsoleKeyInfo = consoleKeyInfo;
+ _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
+
+ if (Console.KeyAvailable)
+ {
+ continue;
+ }
+
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+ _cki = null;
+ _isEscSeq = false;
+ }
+
+ break;
+ }
+
+ if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
+ {
+ ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
+ _cki = null;
+
+ if (Console.KeyAvailable)
+ {
+ _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
+ }
+ else
+ {
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+ }
+
+ break;
+ }
+
+ ProcessMapConsoleKeyInfo (consoleKeyInfo);
+
+ if (_retries > 0)
+ {
+ _retries = 0;
+ }
+
+ break;
+ }
+ }
+
+ _inputReady.Set ();
+ }
+
+ void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
+ {
+ _inputQueue.Enqueue (
+ new InputResult
+ {
+ EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
+ }
+ );
+ _isEscSeq = false;
+ }
+ }
+
+ private void CheckWindowSizeChange ()
+ {
+ void RequestWindowSize (CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ // Wait for a while then check if screen has changed sizes
+ Task.Delay (500, cancellationToken).Wait (cancellationToken);
+
+ int buffHeight, buffWidth;
+
+ if (((NetDriver)_consoleDriver).IsWinPlatform)
+ {
+ buffHeight = Math.Max (Console.BufferHeight, 0);
+ buffWidth = Math.Max (Console.BufferWidth, 0);
+ }
+ else
+ {
+ buffHeight = _consoleDriver.Rows;
+ buffWidth = _consoleDriver.Cols;
+ }
+
+ if (EnqueueWindowSizeEvent (
+ Math.Max (Console.WindowHeight, 0),
+ Math.Max (Console.WindowWidth, 0),
+ buffHeight,
+ buffWidth
+ ))
+ {
+ return;
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested ();
+ }
+
+ while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
+ {
+ try
+ {
+ _winChange.Wait (_inputReadyCancellationTokenSource.Token);
+ _winChange.Reset ();
+
+ RequestWindowSize (_inputReadyCancellationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ _inputReady.Set ();
+ }
+ }
+
+ /// 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 EscSeqReqStatus seqReqStatus,
+ (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
+ );
+
+ if (isMouse)
+ {
+ foreach (MouseFlags mf in mouseFlags)
+ {
+ HandleMouseEvent (MapMouseFlags (mf), pos);
+ }
+
+ return;
+ }
+
+ if (seqReqStatus is { })
+ {
+ //HandleRequestResponseEvent (c1Control, code, values, terminating);
+
+ var ckiString = EscSeqUtils.ToString (cki);
+
+ lock (seqReqStatus.AnsiRequest._responseLock)
+ {
+ seqReqStatus.AnsiRequest.Response = ckiString;
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, ckiString);
+ }
+
+ return;
+ }
+
+ if (!string.IsNullOrEmpty (EscSeqUtils.InvalidRequestTerminator))
+ {
+ if (EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus result))
+ {
+ lock (result.AnsiRequest._responseLock)
+ {
+ result.AnsiRequest.Response = EscSeqUtils.InvalidRequestTerminator;
+ result.AnsiRequest.RaiseResponseFromInput (result.AnsiRequest, EscSeqUtils.InvalidRequestTerminator);
+
+ EscSeqUtils.InvalidRequestTerminator = null;
+ }
+ }
+
+ return;
+ }
+
+ if (newConsoleKeyInfo != default)
+ {
+ HandleKeyboardEvent (newConsoleKeyInfo);
+ }
+ }
+
+ [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
+ private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
+ {
+ MouseButtonState mbs = default;
+
+ foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
+ {
+ if (mouseFlags.HasFlag ((MouseFlags)flag))
+ {
+ switch (flag)
+ {
+ case MouseFlags.Button1Pressed:
+ mbs |= MouseButtonState.Button1Pressed;
+
+ break;
+ case MouseFlags.Button1Released:
+ mbs |= MouseButtonState.Button1Released;
+
+ break;
+ case MouseFlags.Button1Clicked:
+ mbs |= MouseButtonState.Button1Clicked;
+
+ break;
+ case MouseFlags.Button1DoubleClicked:
+ mbs |= MouseButtonState.Button1DoubleClicked;
+
+ break;
+ case MouseFlags.Button1TripleClicked:
+ mbs |= MouseButtonState.Button1TripleClicked;
+
+ break;
+ case MouseFlags.Button2Pressed:
+ mbs |= MouseButtonState.Button2Pressed;
+
+ break;
+ case MouseFlags.Button2Released:
+ mbs |= MouseButtonState.Button2Released;
+
+ break;
+ case MouseFlags.Button2Clicked:
+ mbs |= MouseButtonState.Button2Clicked;
+
+ break;
+ case MouseFlags.Button2DoubleClicked:
+ mbs |= MouseButtonState.Button2DoubleClicked;
+
+ break;
+ case MouseFlags.Button2TripleClicked:
+ mbs |= MouseButtonState.Button2TripleClicked;
+
+ break;
+ case MouseFlags.Button3Pressed:
+ mbs |= MouseButtonState.Button3Pressed;
+
+ break;
+ case MouseFlags.Button3Released:
+ mbs |= MouseButtonState.Button3Released;
+
+ break;
+ case MouseFlags.Button3Clicked:
+ mbs |= MouseButtonState.Button3Clicked;
+
+ break;
+ case MouseFlags.Button3DoubleClicked:
+ mbs |= MouseButtonState.Button3DoubleClicked;
+
+ break;
+ case MouseFlags.Button3TripleClicked:
+ mbs |= MouseButtonState.Button3TripleClicked;
+
+ break;
+ case MouseFlags.WheeledUp:
+ mbs |= MouseButtonState.ButtonWheeledUp;
+
+ break;
+ case MouseFlags.WheeledDown:
+ mbs |= MouseButtonState.ButtonWheeledDown;
+
+ break;
+ case MouseFlags.WheeledLeft:
+ mbs |= MouseButtonState.ButtonWheeledLeft;
+
+ break;
+ case MouseFlags.WheeledRight:
+ mbs |= MouseButtonState.ButtonWheeledRight;
+
+ break;
+ case MouseFlags.Button4Pressed:
+ mbs |= MouseButtonState.Button4Pressed;
+
+ break;
+ case MouseFlags.Button4Released:
+ mbs |= MouseButtonState.Button4Released;
+
+ break;
+ case MouseFlags.Button4Clicked:
+ mbs |= MouseButtonState.Button4Clicked;
+
+ break;
+ case MouseFlags.Button4DoubleClicked:
+ mbs |= MouseButtonState.Button4DoubleClicked;
+
+ break;
+ case MouseFlags.Button4TripleClicked:
+ mbs |= MouseButtonState.Button4TripleClicked;
+
+ break;
+ case MouseFlags.ButtonShift:
+ mbs |= MouseButtonState.ButtonShift;
+
+ break;
+ case MouseFlags.ButtonCtrl:
+ mbs |= MouseButtonState.ButtonCtrl;
+
+ break;
+ case MouseFlags.ButtonAlt:
+ mbs |= MouseButtonState.ButtonAlt;
+
+ break;
+ case MouseFlags.ReportMousePosition:
+ mbs |= MouseButtonState.ReportMousePosition;
+
+ break;
+ case MouseFlags.AllEvents:
+ mbs |= MouseButtonState.AllEvents;
+
+ break;
+ }
+ }
+ }
+
+ return mbs;
+ }
+
+ private Point _lastCursorPosition;
+
+ //private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
+ //{
+ // if (terminating ==
+
+ // // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
+ // // The observation is correct because the response isn't immediate and this is useless
+ // EscSeqUtils.CSI_RequestCursorPositionReport.Terminator)
+ // {
+ // var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
+
+ // if (_lastCursorPosition.Y != point.Y)
+ // {
+ // _lastCursorPosition = point;
+ // var eventType = EventType.WindowPosition;
+ // var winPositionEv = new WindowPositionEvent { CursorPosition = point };
+
+ // _inputQueue.Enqueue (
+ // new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
+ // );
+ // }
+ // else
+ // {
+ // return;
+ // }
+ // }
+ // else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars.Terminator)
+ // {
+ // if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars.Value)
+ // {
+ // EnqueueWindowSizeEvent (
+ // Math.Max (int.Parse (values [1]), 0),
+ // Math.Max (int.Parse (values [2]), 0),
+ // Math.Max (int.Parse (values [1]), 0),
+ // Math.Max (int.Parse (values [2]), 0)
+ // );
+ // }
+ // else
+ // {
+ // EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+ // }
+ // }
+ // else
+ // {
+ // EnqueueRequestResponseEvent (c1Control, code, values, terminating);
+ // }
+
+ // _inputReady.Set ();
+ //}
+
+ //private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
+ //{
+ // var eventType = EventType.RequestResponse;
+ // var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
+
+ // _inputQueue.Enqueue (
+ // new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
+ // );
+ //}
+
+ private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
+ {
+ var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
+
+ _inputQueue.Enqueue (
+ new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
+ );
+
+ _inputReady.Set ();
+ }
+
+ public enum EventType
+ {
+ Key = 1,
+ Mouse = 2,
+ WindowSize = 3,
+ WindowPosition = 4,
+ RequestResponse = 5
+ }
+
+ [Flags]
+ public enum MouseButtonState
+ {
+ Button1Pressed = 0x1,
+ Button1Released = 0x2,
+ Button1Clicked = 0x4,
+ Button1DoubleClicked = 0x8,
+ Button1TripleClicked = 0x10,
+ Button2Pressed = 0x20,
+ Button2Released = 0x40,
+ Button2Clicked = 0x80,
+ Button2DoubleClicked = 0x100,
+ Button2TripleClicked = 0x200,
+ Button3Pressed = 0x400,
+ Button3Released = 0x800,
+ Button3Clicked = 0x1000,
+ Button3DoubleClicked = 0x2000,
+ Button3TripleClicked = 0x4000,
+ ButtonWheeledUp = 0x8000,
+ ButtonWheeledDown = 0x10000,
+ ButtonWheeledLeft = 0x20000,
+ ButtonWheeledRight = 0x40000,
+ Button4Pressed = 0x80000,
+ Button4Released = 0x100000,
+ Button4Clicked = 0x200000,
+ Button4DoubleClicked = 0x400000,
+ Button4TripleClicked = 0x800000,
+ ButtonShift = 0x1000000,
+ ButtonCtrl = 0x2000000,
+ ButtonAlt = 0x4000000,
+ ReportMousePosition = 0x8000000,
+ AllEvents = -1
+ }
+
+ public struct MouseEvent
+ {
+ public Point Position;
+ public MouseButtonState ButtonState;
+ }
+
+ public struct WindowSizeEvent
+ {
+ public Size Size;
+ }
+
+ public struct WindowPositionEvent
+ {
+ public int Top;
+ public int Left;
+ public Point CursorPosition;
+ }
+
+ public struct RequestResponseEvent
+ {
+ public (string c1Control, string code, string [] values, string terminating) ResultTuple;
+ }
+
+ public struct InputResult
+ {
+ public EventType EventType;
+ public ConsoleKeyInfo ConsoleKeyInfo;
+ public MouseEvent MouseEvent;
+ public WindowSizeEvent WindowSizeEvent;
+ public WindowPositionEvent WindowPositionEvent;
+ public RequestResponseEvent RequestResponseEvent;
+
+ public readonly override string ToString ()
+ {
+ return EventType switch
+ {
+ EventType.Key => ToString (ConsoleKeyInfo),
+ EventType.Mouse => MouseEvent.ToString (),
+
+ //EventType.WindowSize => WindowSize.ToString (),
+ //EventType.RequestResponse => RequestResponse.ToString (),
+ _ => "Unknown event type: " + EventType
+ };
+ }
+
+ /// 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
+ }
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
new file mode 100644
index 000000000..d876ef0cb
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs
@@ -0,0 +1,173 @@
+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 _inputHandlerTokenSource = new ();
+ private readonly ConcurrentQueue _resultQueue = new ();
+ internal 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;
+
+ if (ConsoleDriver.RunningUnitTests)
+ {
+ return;
+ }
+
+ Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
+ }
+
+ void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
+
+ bool IMainLoopDriver.EventsPending ()
+ {
+ _waitForProbe.Set ();
+
+ if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
+ {
+ return true;
+ }
+
+ try
+ {
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+ // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
+ // are no timers, but there IS an idle handler waiting.
+ _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return true;
+ }
+ finally
+ {
+ _eventReady.Reset ();
+ }
+
+ _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
+
+ if (!_eventReadyTokenSource.IsCancellationRequested)
+ {
+ return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
+ }
+
+ return true;
+ }
+
+ void IMainLoopDriver.Iteration ()
+ {
+ while (_resultQueue.Count > 0)
+ {
+ // Always dequeue even if it's null and invoke if isn't null
+ if (_resultQueue.TryDequeue (out NetEvents.InputResult? dequeueResult))
+ {
+ if (dequeueResult is { })
+ {
+ ProcessInput?.Invoke (dequeueResult.Value);
+ }
+ }
+ }
+ }
+
+ void IMainLoopDriver.TearDown ()
+ {
+ _inputHandlerTokenSource?.Cancel ();
+ _inputHandlerTokenSource?.Dispose ();
+ _eventReadyTokenSource?.Cancel ();
+ _eventReadyTokenSource?.Dispose ();
+
+ _eventReady?.Dispose ();
+
+ _resultQueue?.Clear ();
+ _waitForProbe?.Dispose ();
+ _netEvents?.Dispose ();
+ _netEvents = null;
+
+ _mainLoop = null;
+ }
+
+ private void NetInputHandler ()
+ {
+ while (_mainLoop is { })
+ {
+ try
+ {
+ if (!_netEvents._forceRead && !_inputHandlerTokenSource.IsCancellationRequested)
+ {
+ _waitForProbe.Wait (_inputHandlerTokenSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+ finally
+ {
+ if (_waitForProbe.IsSet)
+ {
+ _waitForProbe.Reset ();
+ }
+ }
+
+ if (_inputHandlerTokenSource.IsCancellationRequested)
+ {
+ return;
+ }
+
+ _inputHandlerTokenSource.Token.ThrowIfCancellationRequested ();
+
+ if (_resultQueue.Count == 0)
+ {
+ _resultQueue.Enqueue (_netEvents.DequeueInput ());
+ }
+
+ try
+ {
+ while (_resultQueue.Count > 0 && _resultQueue.TryPeek (out NetEvents.InputResult? result) && result is null)
+ {
+ // Dequeue null values
+ _resultQueue.TryDequeue (out _);
+ }
+ }
+ catch (InvalidOperationException) // Peek can raise an exception
+ { }
+
+ if (_resultQueue.Count > 0)
+ {
+ _eventReady.Set ();
+ }
+ }
+ }
+}
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
new file mode 100644
index 000000000..81a9f6b68
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs
@@ -0,0 +1,125 @@
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui;
+
+internal class NetWinVTConsole
+{
+ private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
+ private const uint ENABLE_ECHO_INPUT = 4;
+ private const uint ENABLE_EXTENDED_FLAGS = 128;
+ private const uint ENABLE_INSERT_MODE = 32;
+ private const uint ENABLE_LINE_INPUT = 2;
+ private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
+ private const uint ENABLE_MOUSE_INPUT = 16;
+
+ // Input modes.
+ private const uint ENABLE_PROCESSED_INPUT = 1;
+
+ // Output modes.
+ private const uint ENABLE_PROCESSED_OUTPUT = 1;
+ private const uint ENABLE_QUICK_EDIT_MODE = 64;
+ private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
+ private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
+ private const uint ENABLE_WINDOW_INPUT = 8;
+ private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
+ private const int STD_ERROR_HANDLE = -12;
+ private const int STD_INPUT_HANDLE = -10;
+ private const int STD_OUTPUT_HANDLE = -11;
+
+ private readonly nint _errorHandle;
+ private readonly nint _inputHandle;
+ private readonly uint _originalErrorConsoleMode;
+ private readonly uint _originalInputConsoleMode;
+ private readonly uint _originalOutputConsoleMode;
+ private readonly nint _outputHandle;
+
+ public NetWinVTConsole ()
+ {
+ _inputHandle = GetStdHandle (STD_INPUT_HANDLE);
+
+ if (!GetConsoleMode (_inputHandle, out uint mode))
+ {
+ throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
+ }
+
+ _originalInputConsoleMode = mode;
+
+ if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
+ {
+ mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
+
+ if (!SetConsoleMode (_inputHandle, mode))
+ {
+ throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
+ }
+ }
+
+ _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
+
+ if (!GetConsoleMode (_outputHandle, out mode))
+ {
+ throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
+ }
+
+ _originalOutputConsoleMode = mode;
+
+ if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
+ {
+ mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
+
+ if (!SetConsoleMode (_outputHandle, mode))
+ {
+ throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
+ }
+ }
+
+ _errorHandle = GetStdHandle (STD_ERROR_HANDLE);
+
+ if (!GetConsoleMode (_errorHandle, out mode))
+ {
+ throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
+ }
+
+ _originalErrorConsoleMode = mode;
+
+ if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
+ {
+ mode |= DISABLE_NEWLINE_AUTO_RETURN;
+
+ if (!SetConsoleMode (_errorHandle, mode))
+ {
+ throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
+ }
+ }
+ }
+
+ public void Cleanup ()
+ {
+ if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))
+ {
+ throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
+ }
+
+ if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode))
+ {
+ throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
+ }
+
+ if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode))
+ {
+ throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
+ }
+ }
+
+ [DllImport ("kernel32.dll")]
+ private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
+
+ [DllImport ("kernel32.dll")]
+ private static extern uint GetLastError ();
+
+ [DllImport ("kernel32.dll", SetLastError = true)]
+ private static extern nint GetStdHandle (int nStdHandle);
+
+ [DllImport ("kernel32.dll")]
+ private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
+}
diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
new file mode 100644
index 000000000..22c0e1035
--- /dev/null
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
@@ -0,0 +1,1109 @@
+// TODO: #nullable enable
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using Terminal.Gui.ConsoleDrivers;
+
+namespace Terminal.Gui;
+
+internal class WindowsConsole
+{
+ 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;
+ }
+
+ 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 (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 (_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;
+ }
+
+ //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 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;
+
+ public InputRecord [] ReadConsoleInput ()
+ {
+ const int bufferSize = 1;
+ InputRecord inputRecord = default;
+ uint numberEventsRead = 0;
+ StringBuilder ansiSequence = new StringBuilder ();
+ bool readingSequence = false;
+ bool raisedResponse = false;
+
+ while (true)
+ {
+ try
+ {
+ // Peek to check if there is any input available
+ if (PeekConsoleInput (_inputHandle, out _, bufferSize, out uint eventsRead) && eventsRead > 0)
+ {
+ // Read the input since it is available
+ ReadConsoleInput (
+ _inputHandle,
+ out inputRecord,
+ bufferSize,
+ 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
+ if (PeekConsoleInput (_inputHandle, out InputRecord peekRecord, bufferSize, out eventsRead) && eventsRead > 0)
+ {
+ if (peekRecord is { EventType: EventType.Key, KeyEvent.bKeyDown: true })
+ {
+ // It's really an ANSI request response
+ readingSequence = true;
+ ansiSequence.Clear (); // Start a new sequence
+ ansiSequence.Append (inputChar);
+
+ continue;
+ }
+ }
+ }
+ else if (readingSequence)
+ {
+ ansiSequence.Append (inputChar);
+
+ // Check if the sequence has ended with an expected command terminator
+ if (_mainLoop.EscSeqRequests is { } && _mainLoop.EscSeqRequests.HasResponse (inputChar.ToString (), out EscSeqReqStatus seqReqStatus))
+ {
+ // Finished reading the sequence and remove the enqueued request
+ _mainLoop.EscSeqRequests.Remove (seqReqStatus);
+
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ raisedResponse = true;
+ seqReqStatus.AnsiRequest.Response = ansiSequence.ToString ();
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, seqReqStatus.AnsiRequest.Response);
+ // Clear the terminator for not be enqueued
+ inputRecord = default (InputRecord);
+ }
+ }
+
+ continue;
+ }
+ }
+ }
+ }
+
+ if (readingSequence && !raisedResponse && EscSeqUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 })
+ {
+ _mainLoop.EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus seqReqStatus);
+
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ seqReqStatus.AnsiRequest.Response = ansiSequence.ToString ();
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, seqReqStatus.AnsiRequest.Response);
+ }
+
+ _retries = 0;
+ }
+ else if (EscSeqUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 })
+ {
+ if (_retries > 1)
+ {
+ if (_mainLoop.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response))
+ {
+ lock (seqReqStatus!.AnsiRequest._responseLock)
+ {
+ _mainLoop.EscSeqRequests.Statuses.TryDequeue (out _);
+
+ seqReqStatus.AnsiRequest.Response = string.Empty;
+ seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
+ }
+ }
+
+ _retries = 0;
+ }
+ else
+ {
+ _retries++;
+ }
+ }
+ else
+ {
+ _retries = 0;
+ }
+
+ return numberEventsRead == 0
+ ? null
+ : [inputRecord];
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+ }
+
+#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.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
similarity index 60%
rename from Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
rename to Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
index 83763e081..6f20539d8 100644
--- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs
@@ -1,4 +1,5 @@
-//
+// TODO: #nullable enable
+//
// WindowsDriver.cs: Windows specific driver
//
@@ -23,1109 +24,6 @@ using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
namespace Terminal.Gui;
-internal class WindowsConsole
-{
- 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;
- }
-
- 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 (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 (_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;
- }
-
- //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", 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;
-
- public InputRecord [] ReadConsoleInput ()
- {
- const int bufferSize = 1;
- InputRecord inputRecord = default;
- uint numberEventsRead = 0;
- StringBuilder ansiSequence = new StringBuilder ();
- bool readingSequence = false;
- bool raisedResponse = false;
-
- while (true)
- {
- try
- {
- // Peek to check if there is any input available
- if (PeekConsoleInput (_inputHandle, out _, bufferSize, out uint eventsRead) && eventsRead > 0)
- {
- // Read the input since it is available
- ReadConsoleInput (
- _inputHandle,
- out inputRecord,
- bufferSize,
- 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
- if (PeekConsoleInput (_inputHandle, out InputRecord peekRecord, bufferSize, out eventsRead) && eventsRead > 0)
- {
- if (peekRecord is { EventType: EventType.Key, KeyEvent.bKeyDown: true })
- {
- // It's really an ANSI request response
- readingSequence = true;
- ansiSequence.Clear (); // Start a new sequence
- ansiSequence.Append (inputChar);
-
- continue;
- }
- }
- }
- else if (readingSequence)
- {
- ansiSequence.Append (inputChar);
-
- // Check if the sequence has ended with an expected command terminator
- if (_mainLoop.EscSeqRequests is { } && _mainLoop.EscSeqRequests.HasResponse (inputChar.ToString (), out EscSeqReqStatus seqReqStatus))
- {
- // Finished reading the sequence and remove the enqueued request
- _mainLoop.EscSeqRequests.Remove (seqReqStatus);
-
- lock (seqReqStatus!.AnsiRequest._responseLock)
- {
- raisedResponse = true;
- seqReqStatus.AnsiRequest.Response = ansiSequence.ToString ();
- seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, seqReqStatus.AnsiRequest.Response);
- // Clear the terminator for not be enqueued
- inputRecord = default (InputRecord);
- }
- }
-
- continue;
- }
- }
- }
- }
-
- if (readingSequence && !raisedResponse && EscSeqUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 })
- {
- _mainLoop.EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus seqReqStatus);
-
- lock (seqReqStatus!.AnsiRequest._responseLock)
- {
- seqReqStatus.AnsiRequest.Response = ansiSequence.ToString ();
- seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, seqReqStatus.AnsiRequest.Response);
- }
-
- _retries = 0;
- }
- else if (EscSeqUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 })
- {
- if (_retries > 1)
- {
- if (_mainLoop.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response))
- {
- lock (seqReqStatus!.AnsiRequest._responseLock)
- {
- _mainLoop.EscSeqRequests.Statuses.TryDequeue (out _);
-
- seqReqStatus.AnsiRequest.Response = string.Empty;
- seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
- }
- }
-
- _retries = 0;
- }
- else
- {
- _retries++;
- }
- }
- else
- {
- _retries = 0;
- }
-
- return numberEventsRead == 0
- ? null
- : [inputRecord];
- }
- catch (Exception)
- {
- return null;
- }
- }
- }
-
-#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;
diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj
index 88d57c26b..abf668f25 100644
--- a/Terminal.Gui/Terminal.Gui.csproj
+++ b/Terminal.Gui/Terminal.Gui.csproj
@@ -164,8 +164,7 @@
-
+
diff --git a/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
index 321af7c2e..f88c80820 100644
--- a/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
+++ b/UICatalog/Scenarios/AnsiEscapeSequenceRequests.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
using Terminal.Gui;
namespace UICatalog.Scenarios;
@@ -12,14 +11,13 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
{
private GraphView _graphView;
- private DateTime start = DateTime.Now;
private ScatterSeries _sentSeries;
private ScatterSeries _answeredSeries;
- private List sends = new ();
+ private readonly List _sends = new ();
- private object lockAnswers = new object ();
- private Dictionary answers = new ();
+ private readonly object _lockAnswers = new ();
+ private readonly Dictionary _answers = new ();
private Label _lblSummary;
public override void Main ()
@@ -27,18 +25,18 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
// Init
Application.Init ();
- TabView tv = new TabView
+ var tv = new TabView
{
Width = Dim.Fill (),
Height = Dim.Fill ()
};
- Tab single = new Tab ();
- single.DisplayText = "Single";
+ var single = new Tab ();
+ single.DisplayText = "_Single";
single.View = BuildSingleTab ();
Tab bulk = new ();
- bulk.DisplayText = "Multi";
+ bulk.DisplayText = "_Multi";
bulk.View = BuildBulkTab ();
tv.AddTab (single, true);
@@ -47,7 +45,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
// Setup - Create a top-level application window and configure it.
Window appWindow = new ()
{
- Title = GetQuitKeyAndName (),
+ Title = GetQuitKeyAndName ()
};
appWindow.Add (tv);
@@ -61,18 +59,20 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
// Shutdown - Calling Application.Shutdown is required.
Application.Shutdown ();
}
+
private View BuildSingleTab ()
{
- View w = new View ()
+ var w = new View
{
- Width = Dim.Fill(),
+ Width = Dim.Fill (),
Height = Dim.Fill (),
CanFocus = true
};
w.Padding.Thickness = new (1);
- var scrRequests = new List
+ // 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",
@@ -80,18 +80,19 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
"CSI_SendDeviceAttributes2"
};
- var cbRequests = new ComboBox () { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) };
+ var cbRequests = new ComboBox { Width = 40, Height = 5, ReadOnly = true, Source = new ListWrapper (new (scrRequests)) };
w.Add (cbRequests);
- var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "Request:" };
+ // TODO: Use Pos.Align and Dim.Func so these hardcoded widths aren't needed.
+ var label = new Label { Y = Pos.Bottom (cbRequests) + 1, Text = "_Request:" };
var tfRequest = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 20 };
w.Add (label, tfRequest);
- label = new Label { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "Value:" };
+ label = new () { X = Pos.Right (tfRequest) + 1, Y = Pos.Top (tfRequest) - 1, Text = "E_xpectedResponseValue:" };
var tfValue = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6 };
w.Add (label, tfValue);
- label = new Label { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "Terminator:" };
+ label = new () { X = Pos.Right (tfValue) + 1, Y = Pos.Top (tfValue) - 1, Text = "_Terminator:" };
var tfTerminator = new TextField { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4 };
w.Add (label, tfTerminator);
@@ -102,101 +103,111 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
return;
}
- var selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
+ string selAnsiEscapeSequenceRequestName = scrRequests [cbRequests.SelectedItem];
AnsiEscapeSequenceRequest selAnsiEscapeSequenceRequest = null;
+
switch (selAnsiEscapeSequenceRequestName)
{
case "CSI_SendDeviceAttributes":
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes;
+
break;
case "CSI_ReportTerminalSizeInChars":
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars;
+
break;
case "CSI_RequestCursorPositionReport":
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_RequestCursorPositionReport;
+
break;
case "CSI_SendDeviceAttributes2":
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_SendDeviceAttributes2;
+
break;
}
tfRequest.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Request : "";
- tfValue.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Value ?? "" : "";
+
+ tfValue.Text = selAnsiEscapeSequenceRequest is { }
+ ? selAnsiEscapeSequenceRequest.ExpectedResponseValue ?? ""
+ : "";
tfTerminator.Text = selAnsiEscapeSequenceRequest is { } ? selAnsiEscapeSequenceRequest.Terminator : "";
};
+
// Forces raise cbRequests.SelectedItemChanged to update TextFields
cbRequests.SelectedItem = 0;
- label = new Label { Y = Pos.Bottom (tfRequest) + 2, Text = "Response:" };
+ label = new () { Y = Pos.Bottom (tfRequest) + 2, Text = "_Response:" };
var tvResponse = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
w.Add (label, tvResponse);
- label = new Label { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "Error:" };
+ label = new () { X = Pos.Right (tvResponse) + 1, Y = Pos.Top (tvResponse) - 1, Text = "_Error:" };
var tvError = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 40, Height = 4, ReadOnly = true };
w.Add (label, tvError);
- label = new Label { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "Value:" };
+ label = new () { X = Pos.Right (tvError) + 1, Y = Pos.Top (tvError) - 1, Text = "E_xpectedResponseValue:" };
var tvValue = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 6, Height = 4, ReadOnly = true };
w.Add (label, tvValue);
- label = new Label { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "Terminator:" };
+ label = new () { X = Pos.Right (tvValue) + 1, Y = Pos.Top (tvValue) - 1, Text = "_Terminator:" };
var tvTerminator = new TextView { X = Pos.Left (label), Y = Pos.Bottom (label), Width = 4, Height = 4, ReadOnly = true };
w.Add (label, tvTerminator);
- var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "Send Request", IsDefault = true };
+ var btnResponse = new Button { X = Pos.Center (), Y = Pos.Bottom (tvResponse) + 2, Text = "_Send Request", IsDefault = true };
var lblSuccess = new Label { X = Pos.Center (), Y = Pos.Bottom (btnResponse) + 1 };
w.Add (lblSuccess);
btnResponse.Accepting += (s, e) =>
- {
- var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest
- {
- Request = tfRequest.Text,
- Terminator = tfTerminator.Text,
- Value = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
- };
+ {
+ var ansiEscapeSequenceRequest = new AnsiEscapeSequenceRequest
+ {
+ Request = tfRequest.Text,
+ Terminator = tfTerminator.Text,
+ ExpectedResponseValue = string.IsNullOrEmpty (tfValue.Text) ? null : tfValue.Text
+ };
- var success = AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (
- ansiEscapeSequenceRequest,
- out AnsiEscapeSequenceResponse ansiEscapeSequenceResponse
- );
+ bool success = AnsiEscapeSequenceRequest.TryRequest (
+ ansiEscapeSequenceRequest,
+ out AnsiEscapeSequenceResponse ansiEscapeSequenceResponse
+ );
- tvResponse.Text = ansiEscapeSequenceResponse.Response;
- tvError.Text = ansiEscapeSequenceResponse.Error;
- tvValue.Text = ansiEscapeSequenceResponse.Value ?? "";
- tvTerminator.Text = ansiEscapeSequenceResponse.Terminator;
+ tvResponse.Text = ansiEscapeSequenceResponse.Response;
+ tvError.Text = ansiEscapeSequenceResponse.Error;
+ tvValue.Text = ansiEscapeSequenceResponse.ExpectedResponseValue ?? "";
+ tvTerminator.Text = ansiEscapeSequenceResponse.Terminator;
- if (success)
- {
- lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"];
- lblSuccess.Text = "Successful";
- }
- else
- {
- lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
- lblSuccess.Text = "Error";
- }
- };
+ if (success)
+ {
+ lblSuccess.ColorScheme = Colors.ColorSchemes ["Base"];
+ lblSuccess.Text = "Success";
+ }
+ else
+ {
+ lblSuccess.ColorScheme = Colors.ColorSchemes ["Error"];
+ lblSuccess.Text = "Error";
+ }
+ };
w.Add (btnResponse);
- w.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "You can send other requests by editing the TextFields." });
+ w.Add (new Label { Y = Pos.Bottom (lblSuccess) + 2, Text = "Send other requests by editing the TextFields." });
return w;
}
private View BuildBulkTab ()
{
- View w = new View ()
+ var w = new View
{
Width = Dim.Fill (),
Height = Dim.Fill (),
CanFocus = true
};
- var lbl = new Label ()
+ var lbl = new Label
{
- Text = "This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends. Responses are in red, queued messages are in green.",
+ Text =
+ "_This scenario tests Ansi request/response processing. Use the TextView to ensure regular user interaction continues as normal during sends. Responses are in red, queued messages are in green.",
Height = 2,
Width = Dim.Fill ()
};
@@ -205,50 +216,49 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
TimeSpan.FromMilliseconds (1000),
() =>
{
- lock (lockAnswers)
+ lock (_lockAnswers)
{
UpdateGraph ();
UpdateResponses ();
}
-
-
return true;
});
- var tv = new TextView ()
+ var tv = new TextView
{
Y = Pos.Bottom (lbl),
Width = Dim.Percent (50),
Height = Dim.Fill ()
};
-
- var lblDar = new Label ()
+ var lblDar = new Label
{
Y = Pos.Bottom (lbl),
X = Pos.Right (tv) + 1,
- Text = "DAR per second",
+ Text = "_DAR per second: "
};
- var cbDar = new NumericUpDown ()
+
+ var cbDar = new NumericUpDown
{
X = Pos.Right (lblDar),
Y = Pos.Bottom (lbl),
- Value = 0,
+ Value = 0
};
cbDar.ValueChanging += (s, e) =>
- {
- if (e.NewValue < 0 || e.NewValue > 20)
- {
- e.Cancel = true;
- }
- };
+ {
+ if (e.NewValue < 0 || e.NewValue > 20)
+ {
+ e.Cancel = true;
+ }
+ };
w.Add (cbDar);
int lastSendTime = Environment.TickCount;
- object lockObj = new object ();
+ var lockObj = new object ();
+
Application.AddTimeout (
TimeSpan.FromMilliseconds (50),
() =>
@@ -272,8 +282,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
return true;
});
-
- _graphView = new GraphView ()
+ _graphView = new ()
{
Y = Pos.Bottom (cbDar),
X = Pos.Right (tv),
@@ -281,7 +290,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
Height = Dim.Fill (1)
};
- _lblSummary = new Label ()
+ _lblSummary = new ()
{
Y = Pos.Bottom (_graphView),
X = Pos.Right (tv),
@@ -299,6 +308,7 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
return w;
}
+
private void UpdateResponses ()
{
_lblSummary.Text = GetSummary ();
@@ -307,32 +317,31 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
private string GetSummary ()
{
- if (answers.Count == 0)
+ if (_answers.Count == 0)
{
return "No requests sent yet";
}
- var last = answers.Last ().Value;
+ string last = _answers.Last ().Value;
- var unique = answers.Values.Distinct ().Count ();
- var total = answers.Count;
+ int unique = _answers.Values.Distinct ().Count ();
+ int total = _answers.Count;
return $"Last:{last} U:{unique} T:{total}";
}
private void SetupGraph ()
{
+ _graphView.Series.Add (_sentSeries = new ());
+ _graphView.Series.Add (_answeredSeries = new ());
- _graphView.Series.Add (_sentSeries = new ScatterSeries ());
- _graphView.Series.Add (_answeredSeries = new ScatterSeries ());
-
- _sentSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightGreen, ColorName16.Black));
- _answeredSeries.Fill = new GraphCellToRender (new Rune ('.'), new Attribute (ColorName16.BrightRed, ColorName16.Black));
+ _sentSeries.Fill = new (new ('.'), new (ColorName16.BrightGreen, ColorName16.Black));
+ _answeredSeries.Fill = new (new ('.'), new (ColorName16.BrightRed, ColorName16.Black));
// Todo:
// _graphView.Annotations.Add (_sentSeries new PathAnnotation {});
- _graphView.CellSize = new PointF (1, 1);
+ _graphView.CellSize = new (1, 1);
_graphView.MarginBottom = 2;
_graphView.AxisX.Increment = 1;
_graphView.AxisX.Text = "Seconds";
@@ -341,40 +350,37 @@ public sealed class AnsiEscapeSequenceRequests : Scenario
private void UpdateGraph ()
{
- _sentSeries.Points = sends
+ _sentSeries.Points = _sends
.GroupBy (ToSeconds)
.Select (g => new PointF (g.Key, g.Count ()))
.ToList ();
- _answeredSeries.Points = answers.Keys
+ _answeredSeries.Points = _answers.Keys
.GroupBy (ToSeconds)
.Select (g => new PointF (g.Key, g.Count ()))
.ToList ();
+
// _graphView.ScrollOffset = new PointF(,0);
if (_sentSeries.Points.Count > 0 || _answeredSeries.Points.Count > 0)
{
_graphView.SetNeedsDisplay ();
}
-
}
- private int ToSeconds (DateTime t)
- {
- return (int)(DateTime.Now - t).TotalSeconds;
- }
+ private int ToSeconds (DateTime t) { return (int)(DateTime.Now - t).TotalSeconds; }
private void SendDar ()
{
- sends.Add (DateTime.Now);
- var result = Application.Driver.WriteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes);
+ _sends.Add (DateTime.Now);
+ string result = Application.Driver.WriteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes);
HandleResponse (result);
}
private void HandleResponse (string response)
{
- lock (lockAnswers)
+ lock (_lockAnswers)
{
- answers.Add (DateTime.Now, response);
+ _answers.Add (DateTime.Now, response);
}
}
}