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); } } }