diff --git a/CommunityToolkitExample/LoginView.cs b/CommunityToolkitExample/LoginView.cs index 7f8fbe53d0..726ab009fe 100644 --- a/CommunityToolkitExample/LoginView.cs +++ b/CommunityToolkitExample/LoginView.cs @@ -59,6 +59,7 @@ public void Receive (Message message) } } SetText(); + // BUGBUG: This should not be needed: Application.LayoutAndDraw (); } diff --git a/NativeAot/Program.cs b/NativeAot/Program.cs index bb0f38bc0d..e9c8cf77f9 100644 --- a/NativeAot/Program.cs +++ b/NativeAot/Program.cs @@ -9,8 +9,8 @@ namespace NativeAot; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")] - [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] + [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] private static void Main (string [] args) { Application.Init (); diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index 29b7f5cd9c..c57f39e589 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -9,7 +9,7 @@ namespace SelfContained; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, ConsoleDriver)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, IConsoleDriver)")] private static void Main (string [] args) { Application.Init (); diff --git a/Terminal.Gui/Application/Application.Driver.cs b/Terminal.Gui/Application/Application.Driver.cs index cdfd51ec51..23844adcc1 100644 --- a/Terminal.Gui/Application/Application.Driver.cs +++ b/Terminal.Gui/Application/Application.Driver.cs @@ -5,14 +5,14 @@ public static partial class Application // Driver abstractions { internal static bool _forceFakeConsole; - /// Gets the that has been selected. See also . - public static ConsoleDriver? Driver { get; internal set; } + /// Gets the that has been selected. See also . + public static IConsoleDriver? Driver { get; internal set; } // BUGBUG: Force16Colors should be nullable. /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be output - /// as long as the selected supports TrueColor. + /// as long as the selected supports TrueColor. /// [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { get; set; } @@ -23,7 +23,7 @@ public static partial class Application // Driver abstractions /// specified, the driver is selected based on the platform. /// /// - /// Note, will override this configuration setting if called + /// Note, will override this configuration setting if called /// with either `driver` or `driverName` specified. /// [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 47c07a19f3..c58e1bf92a 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -10,7 +10,7 @@ public static partial class Application // Initialization (Init/Shutdown) /// Initializes a new instance of Application. /// Call this method once per instance (or after has been called). /// - /// This function loads the right for the platform, Creates a . and + /// This function loads the right for the platform, Creates a . and /// assigns it to /// /// @@ -21,23 +21,23 @@ public static partial class Application // Initialization (Init/Shutdown) /// /// /// The function combines - /// and + /// and /// into a single /// call. An application cam use without explicitly calling - /// . + /// . /// /// - /// The to use. If neither or + /// The to use. If neither or /// are specified the default driver for the platform will be used. /// /// /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the - /// to use. If neither or are + /// to use. If neither or are /// specified the default driver for the platform will be used. /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } + public static void Init (IConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } internal static int MainThreadId { get; set; } = -1; @@ -53,7 +53,7 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal static void InternalInit ( - ConsoleDriver? driver = null, + IConsoleDriver? driver = null, string? driverName = null, bool calledViaRunT = false ) @@ -136,7 +136,7 @@ internal static void InternalInit ( if (driverType is { }) { - Driver = (ConsoleDriver)Activator.CreateInstance (driverType)!; + Driver = (IConsoleDriver)Activator.CreateInstance (driverType)!; } else { @@ -181,7 +181,7 @@ internal static void InternalInit ( private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); } private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); } - /// Gets of list of types that are available. + /// Gets of list of types that are available. /// [RequiresUnreferencedCode ("AOT")] public static List GetDriverTypes () @@ -193,7 +193,7 @@ internal static void InternalInit ( { foreach (Type? type in asm.GetTypes ()) { - if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) + if (typeof (IConsoleDriver).IsAssignableFrom (type) && !type.IsAbstract && type.IsClass) { driverTypes.Add (type); } diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 97b566166c..7205841300 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; public static partial class Application // Keyboard handling { /// - /// Called when the user presses a key (by the ). Raises the cancelable + /// Called when the user presses a key (by the ). Raises the cancelable /// event, then calls on all top level views, and finally /// if the key was not handled, invokes any Application-scoped . /// @@ -116,7 +116,7 @@ public static bool RaiseKeyDownEvent (Key key) public static event EventHandler? KeyDown; /// - /// Called when the user releases a key (by the ). Raises the cancelable + /// Called when the user releases a key (by the ). Raises the cancelable /// event /// then calls on all top level views. Called after . /// @@ -205,7 +205,7 @@ internal static void AddApplicationKeyBindings () Command.Refresh, static () => { - LayoutAndDraw (); + LayoutAndDraw (true); return true; } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 7511f82161..64f7960079 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -305,7 +305,7 @@ internal static bool PositionCursor () /// The created object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func? errorHandler = null, ConsoleDriver? driver = null) { return Run (errorHandler, driver); } + public static Toplevel Run (Func? errorHandler = null, IConsoleDriver? driver = null) { return Run (errorHandler, driver); } /// /// Runs the application by creating a -derived object of type T and calling @@ -323,14 +323,14 @@ internal static bool PositionCursor () /// /// /// - /// The to use. If not specified the default driver for the platform will + /// The to use. If not specified the default driver for the platform will /// be used ( , , or ). Must be /// if has already been called. /// /// The created T object. The caller is responsible for disposing this object. [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static T Run (Func? errorHandler = null, ConsoleDriver? driver = null) + public static T Run (Func? errorHandler = null, IConsoleDriver? driver = null) where T : Toplevel, new() { if (!Initialized) @@ -369,7 +369,7 @@ public static T Run (Func? errorHandler = null, ConsoleDrive /// return control immediately. /// /// When using or - /// + /// /// will be called automatically. /// /// @@ -505,6 +505,11 @@ public static void LayoutAndDraw (bool forceDraw = false) { bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size); + if (ClearScreenNextIteration) + { + forceDraw = true; + ClearScreenNextIteration = false; + } if (forceDraw) { Driver?.ClearContents (); @@ -688,6 +693,6 @@ public static void End (RunState runState) runState.Toplevel = null; runState.Dispose (); - LayoutAndDraw (); + LayoutAndDraw (true); } } diff --git a/Terminal.Gui/Application/Application.Screen.cs b/Terminal.Gui/Application/Application.Screen.cs index c5bf6d6fd0..68e7bec3fc 100644 --- a/Terminal.Gui/Application/Application.Screen.cs +++ b/Terminal.Gui/Application/Application.Screen.cs @@ -6,11 +6,11 @@ public static partial class Application // Screen related stuff private static Rectangle? _screen; /// - /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the . + /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the . /// /// /// - /// If the has not been initialized, this will return a default size of 2048x2048; useful for unit tests. + /// If the has not been initialized, this will return a default size of 2048x2048; useful for unit tests. /// /// public static Rectangle Screen @@ -63,8 +63,17 @@ public static bool OnSizeChanging (SizeChangedEventArgs args) t.SetNeedsLayout (); } - LayoutAndDraw (); + LayoutAndDraw (true); return true; } + + /// + /// Gets or sets whether the screen will be cleared, and all Views redrawn, during the next Application iteration. + /// + /// + /// This is typicall set to true when a View's changes and that view has no + /// SuperView (e.g. when is moved or resized. + /// + public static bool ClearScreenNextIteration { get; set; } } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index d9e6c68d92..77a53de2a4 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -32,7 +32,7 @@ public static partial class Application /// A string representation of the Application public new static string ToString () { - ConsoleDriver? driver = Driver; + IConsoleDriver? driver = Driver; if (driver is null) { @@ -43,11 +43,11 @@ public static partial class Application } /// - /// Gets a string representation of the Application rendered by the provided . + /// Gets a string representation of the Application rendered by the provided . /// /// The driver to use to render the contents. /// A string representation of the Application - public static string ToString (ConsoleDriver? driver) + public static string ToString (IConsoleDriver? driver) { if (driver is null) { @@ -215,6 +215,8 @@ internal static void ResetState (bool ignoreDisposed = false) Navigation = null; + ClearScreenNextIteration = false; + AddApplicationKeyBindings (); // Reset synchronization context to allow the user to run async/await, diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index ee4bba220c..2d463ab70e 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -1,4 +1,5 @@ -// +#nullable enable +// // MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui // // Authors: @@ -36,7 +37,7 @@ internal interface IMainLoopDriver /// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this /// on Windows. /// -internal class MainLoop : IDisposable +public class MainLoop : IDisposable { internal List> _idleHandlers = new (); internal SortedList _timeouts = new (); @@ -49,7 +50,7 @@ internal class MainLoop : IDisposable /// Creates a new MainLoop. /// Use to release resources. /// - /// The instance (one of the implementations FakeMainLoop, UnixMainLoop, + /// The instance (one of the implementations FakeMainLoop, UnixMainLoop, /// NetMainLoop or WindowsMainLoop). /// internal MainLoop (IMainLoopDriver driver) @@ -72,7 +73,7 @@ internal ReadOnlyCollection> IdleHandlers /// The current in use. /// The main loop driver. - internal IMainLoopDriver MainLoopDriver { get; private set; } + internal IMainLoopDriver? MainLoopDriver { get; private set; } /// Used for unit tests. internal bool Running { get; set; } @@ -117,7 +118,7 @@ internal Func AddIdle (Func idleHandler) _idleHandlers.Add (idleHandler); } - MainLoopDriver.Wakeup (); + MainLoopDriver?.Wakeup (); return idleHandler; } @@ -130,10 +131,7 @@ internal Func AddIdle (Func idleHandler) /// internal object AddTimeout (TimeSpan time, Func callback) { - if (callback is null) - { - throw new ArgumentNullException (nameof (callback)); - } + ArgumentNullException.ThrowIfNull (callback); var timeout = new Timeout { Span = time, Callback = callback }; AddTimeout (time, timeout); @@ -156,7 +154,7 @@ internal bool CheckTimersAndIdleHandlers (out int waitTimeout) waitTimeout = 0; - lock (_timeouts) + lock (_timeoutsLockToken) { if (_timeouts.Count > 0) { @@ -191,7 +189,7 @@ internal bool CheckTimersAndIdleHandlers (out int waitTimeout) /// You can use this method if you want to probe if events are pending. Typically used if you need to flush the /// input queue while still running some of your own code in your main thread. /// - internal bool EventsPending () { return MainLoopDriver.EventsPending (); } + internal bool EventsPending () { return MainLoopDriver!.EventsPending (); } /// Removes an idle handler added with from processing. /// A token returned by @@ -225,7 +223,7 @@ internal bool RemoveTimeout (object token) { lock (_timeoutsLockToken) { - int idx = _timeouts.IndexOfValue (token as Timeout); + int idx = _timeouts.IndexOfValue ((token as Timeout)!); if (idx == -1) { @@ -262,7 +260,7 @@ internal void Run () /// internal void RunIteration () { - lock (_timeouts) + lock (_timeoutsLockToken) { if (_timeouts.Count > 0) { @@ -270,9 +268,9 @@ internal void RunIteration () } } - MainLoopDriver.Iteration (); + MainLoopDriver?.Iteration (); - var runIdle = false; + bool runIdle; lock (_idleHandlersLock) { @@ -296,8 +294,7 @@ internal void Stop () /// Invoked when a new timeout is added. To be used in the case when /// is . /// - [CanBeNull] - internal event EventHandler TimeoutAdded; + internal event EventHandler? TimeoutAdded; /// Wakes up the that might be waiting on input. internal void Wakeup () { MainLoopDriver?.Wakeup (); } diff --git a/Terminal.Gui/Clipboard/Clipboard.cs b/Terminal.Gui/Clipboard/Clipboard.cs index f8bf907c75..ecb59205f1 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -111,7 +111,7 @@ public static bool TrySetClipboardData (string text) /// /// Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by -/// CursesDriver, but also used in Unit tests which is why it is in ConsoleDriver.cs. +/// CursesDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs. /// internal static class ClipboardProcessRunner { diff --git a/Terminal.Gui/Clipboard/ClipboardBase.cs b/Terminal.Gui/Clipboard/ClipboardBase.cs index 8c01e9c437..8c1c0a93a7 100644 --- a/Terminal.Gui/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Clipboard/ClipboardBase.cs @@ -103,7 +103,7 @@ public bool TrySetClipboardData (string text) } /// - /// Returns the contents of the OS clipboard if possible. Implemented by -specific + /// Returns the contents of the OS clipboard if possible. Implemented by -specific /// subclasses. /// /// The contents of the OS clipboard if successful. @@ -111,7 +111,7 @@ public bool TrySetClipboardData (string text) protected abstract string GetClipboardDataImpl (); /// - /// Pastes the to the OS clipboard if possible. Implemented by + /// Pastes the to the OS clipboard if possible. Implemented by /// -specific subclasses. /// /// The text to paste to the OS clipboard. diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index e1e1730011..f64de17fc9 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; @@ -13,24 +10,56 @@ namespace Terminal.Gui; /// - that uses the .NET Console API - /// for unit testing. /// -public abstract class ConsoleDriver +public abstract class ConsoleDriver : IConsoleDriver { + /// + /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. + /// + /// public ColorTests () + /// { + /// ConsoleDriver.RunningUnitTests = true; + /// } + /// + /// + internal static bool RunningUnitTests { get; set; } + + /// Get the operating system clipboard. + public IClipboard? Clipboard { get; internal set; } + + /// Returns the name of the driver and relevant library version information. + /// + public virtual string GetVersionInfo () { return GetType ().Name; } + + #region ANSI Esc Sequence Handling + + // 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; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// Gets the location and size of the terminal screen. - internal Rectangle Screen => new (0, 0, Cols, Rows); + public Rectangle Screen => new (0, 0, Cols, Rows); - private Region? _clip = null; + private Region? _clip; /// /// Gets or sets the clip rectangle that and are subject /// to. /// /// The rectangle describing the of region. - internal Region? Clip + public Region? Clip { get => _clip; set @@ -50,17 +79,14 @@ internal Region? Clip } } - /// Get the operating system clipboard. - public IClipboard? Clipboard { get; internal set; } - /// /// Gets the column last set by . and are used by /// and to determine where to add content. /// - internal int Col { get; private set; } + public int Col { get; private set; } /// The number of columns visible in the terminal. - internal virtual int Cols + public virtual int Cols { get => _cols; set @@ -75,19 +101,56 @@ internal virtual int Cols /// is called. /// The format of the array is rows, columns. The first index is the row, the second index is the column. /// - internal Cell [,]? Contents { get; set; } + public Cell [,]? Contents { get; set; } /// The leftmost column in the terminal. - internal virtual int Left { get; set; } = 0; + public virtual int Left { get; set; } = 0; + + /// Tests if the specified rune is supported by the driver. + /// + /// + /// if the rune can be properly presented; if the driver does not + /// support displaying this rune. + /// + public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } + + /// Tests whether the specified coordinate are valid for drawing. + /// The column. + /// The row. + /// + /// if the coordinate is outside the screen bounds or outside of . + /// otherwise. + /// + public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); } + + /// + /// Updates and to the specified column and row in . + /// Used by and to determine where to add content. + /// + /// + /// This does not move the cursor on the screen, it only updates the internal state of the driver. + /// + /// If or are negative or beyond and + /// , the method still sets those properties. + /// + /// + /// Column to move to. + /// Row to move to. + public virtual void Move (int col, int row) + { + //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0)); + Col = col; + Row = row; + } /// /// Gets the row last set by . and are used by /// and to determine where to add content. /// - internal int Row { get; private set; } + public int Row { get; private set; } /// The number of rows visible in the terminal. - internal virtual int Rows + public virtual int Rows { get => _rows; set @@ -98,18 +161,7 @@ internal virtual int Rows } /// The topmost row in the terminal. - internal virtual int Top { get; set; } = 0; - - /// - /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. - /// - /// public ColorTests () - /// { - /// ConsoleDriver.RunningUnitTests = true; - /// } - /// - /// - internal static bool RunningUnitTests { get; set; } + public virtual int Top { get; set; } = 0; /// Adds the specified rune to the display at the current cursor position. /// @@ -125,7 +177,7 @@ internal virtual int Rows /// /// /// Rune to add. - internal void AddRune (Rune rune) + public void AddRune (Rune rune) { int runeWidth = -1; bool validLocation = IsValidLocation (rune, Col, Row); @@ -151,7 +203,7 @@ internal void AddRune (Rune rune) // are correctly combined with the base char, but are ALSO treated as 1 column // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. // - // Until this is addressed (see Issue #), we do our best by + // Until this is addressed (see Issue #), we do our best by // a) Attempting to normalize any CM with the base char to it's left // b) Ignoring any CMs that don't normalize if (Col > 0) @@ -174,7 +226,7 @@ internal void AddRune (Rune rune) if (normalized.Length == 1) { // It normalized! We can just set the Cell to the left with the - // normalized codepoint + // normalized codepoint Contents [Row, Col - 1].Rune = (Rune)normalized [0]; // Ignore. Don't move to next column because we're already there @@ -300,7 +352,7 @@ internal void AddRune (Rune rune) /// convenience method that calls with the constructor. /// /// Character to add. - internal void AddRune (char c) { AddRune (new Rune (c)); } + public void AddRune (char c) { AddRune (new Rune (c)); } /// Adds the to the display at the cursor position. /// @@ -312,7 +364,7 @@ internal void AddRune (Rune rune) /// If requires more columns than are available, the output will be clipped. /// /// String. - internal void AddStr (string str) + public void AddStr (string str) { List runes = str.EnumerateRunes ().ToList (); @@ -322,8 +374,39 @@ internal void AddStr (string str) } } + /// Fills the specified rectangle with the specified rune, using + /// + /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn. + /// + /// The Screen-relative rectangle. + /// The Rune used to fill the rectangle + public void FillRect (Rectangle rect, Rune rune = default) + { + // BUGBUG: This should be a method on Region + rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); + lock (Contents!) + { + for (int r = rect.Y; r < rect.Y + rect.Height; r++) + { + for (int c = rect.X; c < rect.X + rect.Width; c++) + { + if (!IsValidLocation (rune, c, r)) + { + continue; + } + Contents [r, c] = new Cell + { + Rune = rune != default ? rune : (Rune)' ', + Attribute = CurrentAttribute, IsDirty = true + }; + _dirtyLines! [r] = true; + } + } + } + } + /// Clears the of the driver. - internal void ClearContents () + public void ClearContents () { Contents = new Cell [Rows, Cols]; @@ -339,13 +422,14 @@ internal void ClearContents () { for (var c = 0; c < Cols; c++) { - Contents [row, c] = new Cell + Contents [row, c] = new () { Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), IsDirty = true }; } + _dirtyLines [row] = true; } } @@ -362,7 +446,7 @@ internal void ClearContents () /// Sets as dirty for situations where views /// don't need layout and redrawing, but just refresh the screen. /// - internal void SetContentsAsDirty () + public void SetContentsAsDirty () { lock (Contents!) { @@ -372,42 +456,8 @@ internal void SetContentsAsDirty () { Contents [row, c].IsDirty = true; } - _dirtyLines! [row] = true; - } - } - } - - /// Determines if the terminal cursor should be visible or not and sets it accordingly. - /// upon success - public abstract bool EnsureCursorVisibility (); - /// Fills the specified rectangle with the specified rune, using - /// - /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn. - /// - /// The Screen-relative rectangle. - /// The Rune used to fill the rectangle - internal void FillRect (Rectangle rect, Rune rune = default) - { - // BUGBUG: This should be a method on Region - rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); - lock (Contents!) - { - for (int r = rect.Y; r < rect.Y + rect.Height; r++) - { - for (int c = rect.X; c < rect.X + rect.Width; c++) - { - if (!IsValidLocation (rune, c, r)) - { - continue; - } - Contents [r, c] = new Cell - { - Rune = (rune != default ? rune : (Rune)' '), - Attribute = CurrentAttribute, IsDirty = true - }; - _dirtyLines! [r] = true; - } + _dirtyLines! [row] = true; } } } @@ -418,25 +468,21 @@ internal void FillRect (Rectangle rect, Rune rune = default) /// /// /// - internal void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } + 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 specified Rune. /// Used to determine if one or two columns are required. /// The column. @@ -445,7 +491,7 @@ internal void FillRect (Rectangle rect, Rune rune = default) /// if the coordinate is outside the screen bounds or outside of . /// otherwise. /// - internal bool IsValidLocation (Rune rune, int col, int row) + public bool IsValidLocation (Rune rune, int col, int row) { if (rune.GetColumns () < 2) { @@ -458,33 +504,12 @@ internal bool IsValidLocation (Rune rune, int col, int row) } } - // TODO: Make internal once Menu is upgraded - /// - /// Updates and to the specified column and row in . - /// Used by and to determine where to add content. - /// - /// - /// This does not move the cursor on the screen, it only updates the internal state of the driver. - /// - /// If or are negative or beyond and - /// , the method still sets those properties. - /// - /// - /// Column to move to. - /// Row to move to. - public virtual void Move (int col, int row) - { - //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0)); - Col = col; - Row = row; - } - /// Called when the terminal size changes. Fires the event. /// - internal void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } + 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 - internal void Refresh () + public void Refresh () { bool updated = UpdateScreen (); UpdateCursor (); @@ -505,6 +530,8 @@ internal void Refresh () /// The event fired when the terminal is resized. public event EventHandler? SizeChanged; + #endregion Cursor Handling + /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. /// This is only implemented in . public abstract void Suspend (); @@ -520,10 +547,10 @@ internal void Refresh () /// Initializes the driver /// Returns an instance of using the for the driver. - internal abstract MainLoop Init (); + public abstract MainLoop Init (); /// Ends the execution of the console driver. - internal abstract void End (); + public abstract void End (); #endregion @@ -544,7 +571,7 @@ internal void Refresh () /// , indicating that the cannot support TrueColor. /// /// - internal virtual bool Force16Colors + public virtual bool Force16Colors { get => Application.Force16Colors || !SupportsTrueColor; set => Application.Force16Colors = value || !SupportsTrueColor; @@ -566,7 +593,7 @@ public Attribute CurrentAttribute // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. if (Application.Driver is { }) { - _currentAttribute = new Attribute (value.Foreground, value.Background); + _currentAttribute = new (value.Foreground, value.Background); return; } @@ -578,7 +605,7 @@ public Attribute CurrentAttribute /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. /// Implementations should call base.SetAttribute(c). /// C. - internal Attribute SetAttribute (Attribute c) + public Attribute SetAttribute (Attribute c) { Attribute prevAttribute = CurrentAttribute; CurrentAttribute = c; @@ -588,7 +615,7 @@ internal Attribute SetAttribute (Attribute c) /// Gets the current . /// The current attribute. - internal Attribute GetAttribute () { return CurrentAttribute; } + public Attribute GetAttribute () { return CurrentAttribute; } // TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be // removed (and Attribute can lose the platformColor property). @@ -599,16 +626,33 @@ internal Attribute SetAttribute (Attribute c) public virtual Attribute MakeColor (in Color foreground, in Color background) { // Encode the colors into the int value. - return new Attribute ( - -1, // only used by cursesdriver! - foreground, - background - ); + return new ( + -1, // only used by cursesdriver! + foreground, + background + ); } - #endregion + #endregion Color Handling + + #region Mouse Handling + + /// Event fired when a mouse event occurs. + public event EventHandler? MouseEvent; + + /// Called when a mouse event occurs. Fires the event. + /// + public void OnMouseEvent (MouseEventArgs a) + { + // Ensure ScreenPosition is set + a.ScreenPosition = a.Position; + + MouseEvent?.Invoke (this, a); + } + + #endregion Mouse Handling - #region Mouse and Keyboard + #region Keyboard Handling /// Event fired when a key is pressed down. This is a precursor to . public event EventHandler? KeyDown; @@ -635,19 +679,8 @@ public virtual Attribute MakeColor (in Color foreground, in Color background) /// public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } - /// Event fired when a mouse event occurs. - public event EventHandler? MouseEvent; - - /// Called when a mouse event occurs. Fires the event. - /// - public void OnMouseEvent (MouseEventArgs a) - { - // Ensure ScreenPosition is set - a.ScreenPosition = a.Position; - - MouseEvent?.Invoke (this, a); - } - + // TODO: Remove this API - it was needed when we didn't have a reliable way to simulate key presses. + // TODO: We now do: Applicaiton.RaiseKeyDown and Application.RaiseKeyUp /// Simulates a key press. /// The key character. /// The key. @@ -656,324 +689,5 @@ public void OnMouseEvent (MouseEventArgs a) /// If simulates the Ctrl key being pressed. public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); - #endregion -} - -/// -/// The enumeration encodes key information from s and provides a -/// consistent way for application code to specify keys and receive key events. -/// -/// The class provides a higher-level abstraction, with helper methods and properties for -/// common operations. For example, and provide a convenient way -/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed. -/// -/// -/// -/// -/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a -/// keyboard. Enum values are provided for these (e.g. , , etc.). -/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent -/// *lowercase*, un-shifted characters. -/// -/// -/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. , -/// , etc.). -/// -/// -/// The shift modifiers (, , and -/// ) can be combined (with logical or) with the other key codes to represent shifted -/// keys. For example, the enum value represents the un-shifted 'a' key, while -/// | represents the 'A' key (shifted 'a' key). Likewise, -/// | represents the 'Alt+A' key combination. -/// -/// -/// All other keys that produce a printable character are encoded as the Unicode value of the character. For -/// example, the for the '!' character is 33, which is the Unicode value for '!'. Likewise, -/// `â` is 226, `Â` is 194, etc. -/// -/// -/// If the is set, then the value is that of the special mask, otherwise, the value is -/// the one of the lower bits (as extracted by ). -/// -/// -[Flags] -public enum KeyCode : uint -{ - /// - /// Mask that indicates that the key is a unicode codepoint. Values outside this range indicate the key has shift - /// modifiers or is a special key like function keys, arrows keys and so on. - /// - CharMask = 0x_f_ffff, - - /// - /// If the is set, then the value is that of the special mask, otherwise, the value is - /// in the lower bits (as extracted by ). - /// - SpecialMask = 0x_fff0_0000, - - /// - /// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by - /// removing the ShiftMask. - /// - ShiftMask = 0x_1000_0000, - - /// - /// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by - /// removing the AltMask. - /// - AltMask = 0x_8000_0000, - - /// - /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by - /// removing the CtrlMask. - /// - CtrlMask = 0x_4000_0000, - - /// The key code representing an invalid or empty key. - Null = 0, - - /// Backspace key. - Backspace = 8, - - /// The key code for the tab key (forwards tab key). - Tab = 9, - - /// The key code for the return key. - Enter = ConsoleKey.Enter, - - /// The key code for the clear key. - Clear = 12, - - /// The key code for the escape key. - Esc = 27, - - /// The key code for the space bar key. - Space = 32, - - /// Digit 0. - D0 = 48, - - /// Digit 1. - D1, - - /// Digit 2. - D2, - - /// Digit 3. - D3, - - /// Digit 4. - D4, - - /// Digit 5. - D5, - - /// Digit 6. - D6, - - /// Digit 7. - D7, - - /// Digit 8. - D8, - - /// Digit 9. - D9, - - /// The key code for the A key - A = 65, - - /// The key code for the B key - B, - - /// The key code for the C key - C, - - /// The key code for the D key - D, - - /// The key code for the E key - E, - - /// The key code for the F key - F, - - /// The key code for the G key - G, - - /// The key code for the H key - H, - - /// The key code for the I key - I, - - /// The key code for the J key - J, - - /// The key code for the K key - K, - - /// The key code for the L key - L, - - /// The key code for the M key - M, - - /// The key code for the N key - N, - - /// The key code for the O key - O, - - /// The key code for the P key - P, - - /// The key code for the Q key - Q, - - /// The key code for the R key - R, - - /// The key code for the S key - S, - - /// The key code for the T key - T, - - /// The key code for the U key - U, - - /// The key code for the V key - V, - - /// The key code for the W key - W, - - /// The key code for the X key - X, - - /// The key code for the Y key - Y, - - /// The key code for the Z key - Z, - - ///// - ///// The key code for the Delete key. - ///// - //Delete = 127, - - // --- Special keys --- - // The values below are common non-alphanum keys. Their values are - // based on the .NET ConsoleKey values, which, in-turn are based on the - // VK_ values from the Windows API. - // We add MaxCodePoint to avoid conflicts with the Unicode values. - - /// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys. - MaxCodePoint = 0x10FFFF, - - /// Cursor up key - CursorUp = MaxCodePoint + ConsoleKey.UpArrow, - - /// Cursor down key. - CursorDown = MaxCodePoint + ConsoleKey.DownArrow, - - /// Cursor left key. - CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow, - - /// Cursor right key. - CursorRight = MaxCodePoint + ConsoleKey.RightArrow, - - /// Page Up key. - PageUp = MaxCodePoint + ConsoleKey.PageUp, - - /// Page Down key. - PageDown = MaxCodePoint + ConsoleKey.PageDown, - - /// Home key. - Home = MaxCodePoint + ConsoleKey.Home, - - /// End key. - End = MaxCodePoint + ConsoleKey.End, - - /// Insert (INS) key. - Insert = MaxCodePoint + ConsoleKey.Insert, - - /// Delete (DEL) key. - Delete = MaxCodePoint + ConsoleKey.Delete, - - /// Print screen character key. - PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen, - - /// F1 key. - F1 = MaxCodePoint + ConsoleKey.F1, - - /// F2 key. - F2 = MaxCodePoint + ConsoleKey.F2, - - /// F3 key. - F3 = MaxCodePoint + ConsoleKey.F3, - - /// F4 key. - F4 = MaxCodePoint + ConsoleKey.F4, - - /// F5 key. - F5 = MaxCodePoint + ConsoleKey.F5, - - /// F6 key. - F6 = MaxCodePoint + ConsoleKey.F6, - - /// F7 key. - F7 = MaxCodePoint + ConsoleKey.F7, - - /// F8 key. - F8 = MaxCodePoint + ConsoleKey.F8, - - /// F9 key. - F9 = MaxCodePoint + ConsoleKey.F9, - - /// F10 key. - F10 = MaxCodePoint + ConsoleKey.F10, - - /// F11 key. - F11 = MaxCodePoint + ConsoleKey.F11, - - /// F12 key. - F12 = MaxCodePoint + ConsoleKey.F12, - - /// F13 key. - F13 = MaxCodePoint + ConsoleKey.F13, - - /// F14 key. - F14 = MaxCodePoint + ConsoleKey.F14, - - /// F15 key. - F15 = MaxCodePoint + ConsoleKey.F15, - - /// F16 key. - F16 = MaxCodePoint + ConsoleKey.F16, - - /// F17 key. - F17 = MaxCodePoint + ConsoleKey.F17, - - /// F18 key. - F18 = MaxCodePoint + ConsoleKey.F18, - - /// F19 key. - F19 = MaxCodePoint + ConsoleKey.F19, - - /// F20 key. - F20 = MaxCodePoint + ConsoleKey.F20, - - /// F21 key. - F21 = MaxCodePoint + ConsoleKey.F21, - - /// F22 key. - F22 = MaxCodePoint + ConsoleKey.F22, - - /// F23 key. - F23 = MaxCodePoint + ConsoleKey.F23, - - /// F24 key. - F24 = MaxCodePoint + ConsoleKey.F24 + #endregion Keyboard Handling } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs index c3497f3acd..6fce2e040f 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -3,6 +3,7 @@ namespace Terminal.Gui.ConsoleDrivers; +// QUESTION: This class combines Windows specific code with cross-platform code. Should this be split into two classes? /// Helper class to handle the scan code and virtual key from a . public static class ConsoleKeyMapping { @@ -249,7 +250,7 @@ public static ConsoleModifiers MapToConsoleModifiers (KeyCode key) { var modifiers = new ConsoleModifiers (); - if (key.HasFlag (KeyCode.ShiftMask)) + if (key.HasFlag (KeyCode.ShiftMask) || char.IsUpper ((char)key)) { modifiers |= ConsoleModifiers.Shift; } @@ -590,7 +591,8 @@ internal static uint GetKeyCharFromUnicodeChar ( if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) { - consoleKey = char.ToUpper (stFormD [i]); + char ck = char.ToUpper (stFormD [i]); + consoleKey = (uint)(ck > 0 && ck <= 255 ? char.ToUpper (stFormD [i]) : 0); scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0); if (scode is { }) @@ -704,6 +706,32 @@ internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsol return (uint)ConsoleKey.F24; case KeyCode.Tab | KeyCode.ShiftMask: return (uint)ConsoleKey.Tab; + case KeyCode.Space: + return (uint)ConsoleKey.Spacebar; + default: + uint c = (char)keyValue; + + if (c is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z) + { + return c; + } + + if ((c - 32) is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z) + { + return (c - 32); + } + + if (Enum.IsDefined (typeof (ConsoleKey), keyValue.ToString ())) + { + return (uint)keyValue; + } + + // DEL + if ((uint)keyValue == 127) + { + return (uint)ConsoleKey.Backspace; + } + break; } isConsoleKey = false; @@ -867,6 +895,14 @@ public static KeyCode MapConsoleKeyInfoToKeyCode (ConsoleKeyInfo consoleKeyInfo) case ConsoleKey.Tab: keyCode = KeyCode.Tab; + break; + case ConsoleKey.Spacebar: + keyCode = KeyCode.Space; + + break; + case ConsoleKey.Backspace: + keyCode = KeyCode.Backspace; + break; default: if ((int)consoleKeyInfo.KeyChar is >= 1 and <= 26) diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 446926fef1..43330eab3b 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -1,25 +1,20 @@ -// +#nullable enable +// // Driver.cs: Curses-based Driver // -using System.Diagnostics; using System.Runtime.InteropServices; using Terminal.Gui.ConsoleDrivers; using Unix.Terminal; namespace Terminal.Gui; -/// This is the Curses driver for the gui.cs/Terminal framework. +/// A Linux/Mac driver based on the Curses library. internal class CursesDriver : ConsoleDriver { - public Curses.Window _window; - private CursorVisibility? _currentCursorVisibility; - private CursorVisibility? _initialCursorVisibility; - private MouseFlags _lastMouseFlags; - private UnixMainLoop _mainLoopDriver; - private object _processInputToken; + public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; } - internal override int Cols + public override int Cols { get => Curses.Cols; set @@ -29,7 +24,7 @@ internal override int Cols } } - internal override int Rows + public override int Rows { get => Curses.Lines; set @@ -39,45 +34,6 @@ internal override int Rows } } - public override bool SupportsTrueColor => true; - - /// - public override bool EnsureCursorVisibility () { return false; } - - /// - public override bool GetCursorVisibility (out CursorVisibility visibility) - { - visibility = CursorVisibility.Invisible; - - if (!_currentCursorVisibility.HasValue) - { - return false; - } - - visibility = _currentCursorVisibility.Value; - - return true; - } - - public override string GetVersionInfo () { return $"{Curses.curses_version ()}"; } - - public static bool Is_WSL_Platform () - { - // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell - //if (new CursesClipboard ().IsSupported) { - // // If xclip is installed on Linux under WSL, this will return true. - // return false; - //} - (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true); - - if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) - { - return true; - } - - return false; - } - public override bool IsRuneSupported (Rune rune) { // See Issue #2615 - CursesDriver is broken with non-BMP characters @@ -101,34 +57,33 @@ public override void Move (int col, int row) { // Not a valid location (outside screen or clip region) // Move within the clip region, then AddRune will actually move to Col, Row - Rectangle clipRect = Clip.GetBounds (); + Rectangle clipRect = Clip!.GetBounds (); Curses.move (clipRect.Y, clipRect.X); } } - public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control) { KeyCode key; if (consoleKey == ConsoleKey.Packet) { - var mod = new ConsoleModifiers (); + //var mod = new ConsoleModifiers (); - if (shift) - { - mod |= ConsoleModifiers.Shift; - } + //if (shift) + //{ + // mod |= ConsoleModifiers.Shift; + //} - if (alt) - { - mod |= ConsoleModifiers.Alt; - } + //if (alt) + //{ + // mod |= ConsoleModifiers.Alt; + //} - if (control) - { - mod |= ConsoleModifiers.Control; - } + //if (control) + //{ + // mod |= ConsoleModifiers.Control; + //} var cKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control); cKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (cKeyInfo); @@ -139,40 +94,12 @@ public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, key = (KeyCode)keyChar; } - OnKeyDown (new Key (key)); - OnKeyUp (new Key (key)); + OnKeyDown (new (key)); + OnKeyUp (new (key)); //OnKeyPressed (new KeyEventArgsEventArgs (key)); } - /// - public override bool SetCursorVisibility (CursorVisibility visibility) - { - if (_initialCursorVisibility.HasValue == false) - { - return false; - } - - if (!RunningUnitTests) - { - Curses.curs_set (((int)visibility >> 16) & 0x000000FF); - } - - if (visibility != CursorVisibility.Invisible) - { - Console.Out.Write ( - EscSeqUtils.CSI_SetCursorStyle ( - (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) - & 0xFF) - ) - ); - } - - _currentCursorVisibility = visibility; - - return true; - } - public void StartReportingMouseMoves () { if (!RunningUnitTests) @@ -213,14 +140,18 @@ public override void UpdateCursor () if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows) { - Curses.move (Row, Col); - if (Force16Colors) { + Curses.move (Row, Col); + Curses.raw (); Curses.noecho (); Curses.refresh (); } + else + { + _mainLoopDriver?.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1)); + } } } @@ -231,7 +162,7 @@ public override bool UpdateScreen () { for (var row = 0; row < Rows; row++) { - if (!_dirtyLines [row]) + if (!_dirtyLines! [row]) { continue; } @@ -240,7 +171,7 @@ public override bool UpdateScreen () for (var col = 0; col < Cols; col++) { - if (Contents [row, col].IsDirty == false) + if (Contents! [row, col].IsDirty == false) { continue; } @@ -284,14 +215,14 @@ public override bool UpdateScreen () if (!RunningUnitTests) { Curses.move (Row, Col); - _window.wrefresh (); + _window?.wrefresh (); } } else { if (RunningUnitTests || Console.WindowHeight < 1 - || Contents.Length != Rows * Cols + || Contents!.Length != Rows * Cols || Rows != Console.WindowHeight) { return updated; @@ -315,7 +246,7 @@ public override bool UpdateScreen () return updated; } - if (!_dirtyLines [row]) + if (!_dirtyLines! [row]) { continue; } @@ -360,7 +291,7 @@ public override bool UpdateScreen () lastCol = col; } - Attribute attr = Contents [row, col].Attribute.Value; + Attribute attr = Contents [row, col].Attribute!.Value; // Performance: Only send the escape sequence if the attribute has changed. if (attr != redrawAttr) @@ -419,10 +350,10 @@ public override bool UpdateScreen () } // SIXELS - foreach (var s in Application.Sixel) + foreach (SixelToRender s in Application.Sixel) { SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Write(s.SixelData); + Console.Write (s.SixelData); } SetCursorPosition (0, 0); @@ -442,40 +373,218 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out return updated; } - private bool SetCursorPosition (int col, int row) + #region Color Handling + + public override bool SupportsTrueColor => true; + + /// Creates an Attribute from the provided curses-based foreground and background color numbers + /// Contains the curses color number for the foreground (color, plus any attributes) + /// Contains the curses color number for the background (color, plus any attributes) + /// + private static Attribute MakeColor (short foreground, short background) { - // + 1 is needed because non-Windows is based on 1 instead of 0 and - // Console.CursorTop/CursorLeft isn't reliable. - Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1)); + //var v = (short)((ushort)foreground | (background << 4)); + var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff)); + + // TODO: for TrueColor - Use InitExtendedPair + Curses.InitColorPair (v, foreground, background); + + return new ( + Curses.ColorPair (v), + CursesColorNumberToColorName16 (foreground), + CursesColorNumberToColorName16 (background) + ); + } + + /// + /// + /// In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4 + /// bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are + /// converted to curses color encoding before being encoded. + /// + public override Attribute MakeColor (in Color foreground, in Color background) + { + if (!RunningUnitTests && Force16Colors) + { + return MakeColor ( + ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()), + ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ()) + ); + } + + return new ( + 0, + foreground, + background + ); + } + + private static short ColorNameToCursesColorNumber (ColorName16 color) + { + switch (color) + { + case ColorName16.Black: + return Curses.COLOR_BLACK; + case ColorName16.Blue: + return Curses.COLOR_BLUE; + case ColorName16.Green: + return Curses.COLOR_GREEN; + case ColorName16.Cyan: + return Curses.COLOR_CYAN; + case ColorName16.Red: + return Curses.COLOR_RED; + case ColorName16.Magenta: + return Curses.COLOR_MAGENTA; + case ColorName16.Yellow: + return Curses.COLOR_YELLOW; + case ColorName16.Gray: + return Curses.COLOR_WHITE; + case ColorName16.DarkGray: + return Curses.COLOR_GRAY; + case ColorName16.BrightBlue: + return Curses.COLOR_BLUE | Curses.COLOR_GRAY; + case ColorName16.BrightGreen: + return Curses.COLOR_GREEN | Curses.COLOR_GRAY; + case ColorName16.BrightCyan: + return Curses.COLOR_CYAN | Curses.COLOR_GRAY; + case ColorName16.BrightRed: + return Curses.COLOR_RED | Curses.COLOR_GRAY; + case ColorName16.BrightMagenta: + return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY; + case ColorName16.BrightYellow: + return Curses.COLOR_YELLOW | Curses.COLOR_GRAY; + case ColorName16.White: + return Curses.COLOR_WHITE | Curses.COLOR_GRAY; + } + + throw new ArgumentException ("Invalid color code"); + } + + private static ColorName16 CursesColorNumberToColorName16 (short color) + { + switch (color) + { + case Curses.COLOR_BLACK: + return ColorName16.Black; + case Curses.COLOR_BLUE: + return ColorName16.Blue; + case Curses.COLOR_GREEN: + return ColorName16.Green; + case Curses.COLOR_CYAN: + return ColorName16.Cyan; + case Curses.COLOR_RED: + return ColorName16.Red; + case Curses.COLOR_MAGENTA: + return ColorName16.Magenta; + case Curses.COLOR_YELLOW: + return ColorName16.Yellow; + case Curses.COLOR_WHITE: + return ColorName16.Gray; + case Curses.COLOR_GRAY: + return ColorName16.DarkGray; + case Curses.COLOR_BLUE | Curses.COLOR_GRAY: + return ColorName16.BrightBlue; + case Curses.COLOR_GREEN | Curses.COLOR_GRAY: + return ColorName16.BrightGreen; + case Curses.COLOR_CYAN | Curses.COLOR_GRAY: + return ColorName16.BrightCyan; + case Curses.COLOR_RED | Curses.COLOR_GRAY: + return ColorName16.BrightRed; + case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY: + return ColorName16.BrightMagenta; + case Curses.COLOR_YELLOW | Curses.COLOR_GRAY: + return ColorName16.BrightYellow; + case Curses.COLOR_WHITE | Curses.COLOR_GRAY: + return ColorName16.White; + } + + throw new ArgumentException ("Invalid curses color code"); + } + + #endregion + + private CursorVisibility? _currentCursorVisibility; + private CursorVisibility? _initialCursorVisibility; + + /// + public override bool EnsureCursorVisibility () + { + if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) + { + GetCursorVisibility (out CursorVisibility cursorVisibility); + _currentCursorVisibility = cursorVisibility; + SetCursorVisibility (CursorVisibility.Invisible); + + return false; + } + + SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default); + + return _currentCursorVisibility == CursorVisibility.Default; + } + + /// + public override bool GetCursorVisibility (out CursorVisibility visibility) + { + visibility = CursorVisibility.Invisible; + + if (!_currentCursorVisibility.HasValue) + { + return false; + } + + visibility = _currentCursorVisibility.Value; return true; } - internal override void End () + /// + public override bool SetCursorVisibility (CursorVisibility visibility) { - StopReportingMouseMoves (); - SetCursorVisibility (CursorVisibility.Default); + if (_initialCursorVisibility.HasValue == false) + { + return false; + } - if (_mainLoopDriver is { }) + if (!RunningUnitTests) { - _mainLoopDriver.RemoveWatch (_processInputToken); + Curses.curs_set (((int)visibility >> 16) & 0x000000FF); } - if (RunningUnitTests) + if (visibility != CursorVisibility.Invisible) { - return; + _mainLoopDriver?.WriteRaw ( + EscSeqUtils.CSI_SetCursorStyle ( + (EscSeqUtils.DECSCUSR_Style) + (((int)visibility >> 24) + & 0xFF) + ) + ); } - // throws away any typeahead that has been typed by - // the user and has not yet been read by the program. - Curses.flushinp (); + _currentCursorVisibility = visibility; - Curses.endwin (); + return true; } - internal override MainLoop Init () + private bool SetCursorPosition (int col, int row) { - _mainLoopDriver = new UnixMainLoop (this); + // + 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 + + private Curses.Window? _window; + private UnixMainLoop? _mainLoopDriver; + private object _processInputToken; + + public override MainLoop Init () + { + _mainLoopDriver = new (this); if (!RunningUnitTests) { @@ -531,19 +640,19 @@ internal override MainLoop Init () Curses.timeout (0); } - _processInputToken = _mainLoopDriver?.AddWatch ( - 0, - UnixMainLoop.Condition.PollIn, - x => - { - ProcessInput (); + _processInputToken = _mainLoopDriver.AddWatch ( + 0, + UnixMainLoop.Condition.PollIn, + x => + { + ProcessInput (); - return true; - } - ); + return true; + } + ); } - CurrentAttribute = new Attribute (ColorName16.White, ColorName16.Black); + CurrentAttribute = new (ColorName16.White, ColorName16.Black); if (Environment.OSVersion.Platform == PlatformID.Win32NT) { @@ -575,13 +684,13 @@ internal override MainLoop Init () { Curses.CheckWinChange (); - if (Force16Colors) - { - Curses.refresh (); - } + // On Init this call is needed no mater Force16Colors or not + Curses.refresh (); + + EscSeqUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed; } - return new MainLoop (_mainLoopDriver); + return new (_mainLoopDriver); } internal void ProcessInput () @@ -841,7 +950,6 @@ ref ConsoleKeyInfo [] cki if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse) { EscSeqUtils.DecodeEscSeq ( - null, ref consoleKeyInfo, ref ck, cki, @@ -854,14 +962,14 @@ ref ConsoleKeyInfo [] cki out List mouseFlags, out Point pos, out _, - ProcessMouseEvent + EscSeqUtils.ProcessMouseEvent ); if (isKeyMouse) { foreach (MouseFlags mf in mouseFlags) { - ProcessMouseEvent (mf, pos); + OnMouseEvent (new () { Flags = mf, Position = pos }); } cki = null; @@ -894,6 +1002,11 @@ ref ConsoleKeyInfo [] cki } } + private void EscSeqUtils_ContinuousButtonPressed (object? sender, MouseEventArgs e) + { + OnMouseEvent (e); + } + private static KeyCode MapCursesKey (int cursesKey) { switch (cursesKey) @@ -971,181 +1084,54 @@ private static KeyCode MapCursesKey (int cursesKey) } } - private void ProcessMouseEvent (MouseFlags mouseFlag, Point pos) + public override void End () { - bool WasButtonReleased (MouseFlags flag) - { - return flag.HasFlag (MouseFlags.Button1Released) - || flag.HasFlag (MouseFlags.Button2Released) - || flag.HasFlag (MouseFlags.Button3Released) - || flag.HasFlag (MouseFlags.Button4Released); - } + EscSeqUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed; - bool IsButtonNotPressed (MouseFlags flag) - { - return !flag.HasFlag (MouseFlags.Button1Pressed) - && !flag.HasFlag (MouseFlags.Button2Pressed) - && !flag.HasFlag (MouseFlags.Button3Pressed) - && !flag.HasFlag (MouseFlags.Button4Pressed); - } + StopReportingMouseMoves (); + SetCursorVisibility (CursorVisibility.Default); - bool IsButtonClickedOrDoubleClicked (MouseFlags flag) + if (_mainLoopDriver is { }) { - return flag.HasFlag (MouseFlags.Button1Clicked) - || flag.HasFlag (MouseFlags.Button2Clicked) - || flag.HasFlag (MouseFlags.Button3Clicked) - || flag.HasFlag (MouseFlags.Button4Clicked) - || flag.HasFlag (MouseFlags.Button1DoubleClicked) - || flag.HasFlag (MouseFlags.Button2DoubleClicked) - || flag.HasFlag (MouseFlags.Button3DoubleClicked) - || flag.HasFlag (MouseFlags.Button4DoubleClicked); + _mainLoopDriver.RemoveWatch (_processInputToken); } - Debug.WriteLine ($"CursesDriver: ({pos.X},{pos.Y}) - {mouseFlag}"); - - - if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0)) + if (RunningUnitTests) { return; } - _lastMouseFlags = mouseFlag; - - var me = new MouseEventArgs { Flags = mouseFlag, Position = pos }; - //Debug.WriteLine ($"CursesDriver: ({me.Position}) - {me.Flags}"); - - OnMouseEvent (me); - } - - #region Color Handling - - /// Creates an Attribute from the provided curses-based foreground and background color numbers - /// Contains the curses color number for the foreground (color, plus any attributes) - /// Contains the curses color number for the background (color, plus any attributes) - /// - private static Attribute MakeColor (short foreground, short background) - { - //var v = (short)((ushort)foreground | (background << 4)); - var v = (short)(((ushort)(foreground & 0xffff) << 16) | (background & 0xffff)); - - // TODO: for TrueColor - Use InitExtendedPair - Curses.InitColorPair (v, foreground, background); + // throws away any typeahead that has been typed by + // the user and has not yet been read by the program. + Curses.flushinp (); - return new Attribute ( - Curses.ColorPair (v), - CursesColorNumberToColorName16 (foreground), - CursesColorNumberToColorName16 (background) - ); + Curses.endwin (); } - /// - /// - /// In the CursesDriver, colors are encoded as an int. The foreground color is stored in the most significant 4 - /// bits, and the background color is stored in the least significant 4 bits. The Terminal.GUi Color values are - /// converted to curses color encoding before being encoded. - /// - public override Attribute MakeColor (in Color foreground, in Color background) - { - if (!RunningUnitTests && Force16Colors) - { - return MakeColor ( - ColorNameToCursesColorNumber (foreground.GetClosestNamedColor16 ()), - ColorNameToCursesColorNumber (background.GetClosestNamedColor16 ()) - ); - } - - return new Attribute ( - 0, - foreground, - background - ); - } + #endregion Init/End/MainLoop - private static short ColorNameToCursesColorNumber (ColorName16 color) + public static bool Is_WSL_Platform () { - switch (color) - { - case ColorName16.Black: - return Curses.COLOR_BLACK; - case ColorName16.Blue: - return Curses.COLOR_BLUE; - case ColorName16.Green: - return Curses.COLOR_GREEN; - case ColorName16.Cyan: - return Curses.COLOR_CYAN; - case ColorName16.Red: - return Curses.COLOR_RED; - case ColorName16.Magenta: - return Curses.COLOR_MAGENTA; - case ColorName16.Yellow: - return Curses.COLOR_YELLOW; - case ColorName16.Gray: - return Curses.COLOR_WHITE; - case ColorName16.DarkGray: - return Curses.COLOR_GRAY; - case ColorName16.BrightBlue: - return Curses.COLOR_BLUE | Curses.COLOR_GRAY; - case ColorName16.BrightGreen: - return Curses.COLOR_GREEN | Curses.COLOR_GRAY; - case ColorName16.BrightCyan: - return Curses.COLOR_CYAN | Curses.COLOR_GRAY; - case ColorName16.BrightRed: - return Curses.COLOR_RED | Curses.COLOR_GRAY; - case ColorName16.BrightMagenta: - return Curses.COLOR_MAGENTA | Curses.COLOR_GRAY; - case ColorName16.BrightYellow: - return Curses.COLOR_YELLOW | Curses.COLOR_GRAY; - case ColorName16.White: - return Curses.COLOR_WHITE | Curses.COLOR_GRAY; - } - - throw new ArgumentException ("Invalid color code"); - } + // xclip does not work on WSL, so we need to use the Windows clipboard vis Powershell + //if (new CursesClipboard ().IsSupported) { + // // If xclip is installed on Linux under WSL, this will return true. + // return false; + //} + (int exitCode, string result) = ClipboardProcessRunner.Bash ("uname -a", waitForOutput: true); - private static ColorName16 CursesColorNumberToColorName16 (short color) - { - switch (color) + if (exitCode == 0 && result.Contains ("microsoft") && result.Contains ("WSL")) { - case Curses.COLOR_BLACK: - return ColorName16.Black; - case Curses.COLOR_BLUE: - return ColorName16.Blue; - case Curses.COLOR_GREEN: - return ColorName16.Green; - case Curses.COLOR_CYAN: - return ColorName16.Cyan; - case Curses.COLOR_RED: - return ColorName16.Red; - case Curses.COLOR_MAGENTA: - return ColorName16.Magenta; - case Curses.COLOR_YELLOW: - return ColorName16.Yellow; - case Curses.COLOR_WHITE: - return ColorName16.Gray; - case Curses.COLOR_GRAY: - return ColorName16.DarkGray; - case Curses.COLOR_BLUE | Curses.COLOR_GRAY: - return ColorName16.BrightBlue; - case Curses.COLOR_GREEN | Curses.COLOR_GRAY: - return ColorName16.BrightGreen; - case Curses.COLOR_CYAN | Curses.COLOR_GRAY: - return ColorName16.BrightCyan; - case Curses.COLOR_RED | Curses.COLOR_GRAY: - return ColorName16.BrightRed; - case Curses.COLOR_MAGENTA | Curses.COLOR_GRAY: - return ColorName16.BrightMagenta; - case Curses.COLOR_YELLOW | Curses.COLOR_GRAY: - return ColorName16.BrightYellow; - case Curses.COLOR_WHITE | Curses.COLOR_GRAY: - return ColorName16.White; + return true; } - throw new ArgumentException ("Invalid curses color code"); + return false; } - #endregion + /// + 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; @@ -1224,4 +1210,4 @@ private static int GetSuspendSignal () [DllImport ("libc")] private static extern int uname (nint buf); -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs index 34139815c5..28df408ea3 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -1,3 +1,4 @@ +#nullable enable // // mainloop.cs: Linux/Curses MainLoop implementation. // @@ -36,21 +37,22 @@ public enum Condition : short PollNval = 32 } - public const int KEY_RESIZE = unchecked ((int)0xffffffffffffffff); + public const int KEY_RESIZE = unchecked((int)0xffffffffffffffff); private static readonly nint _ignore = Marshal.AllocHGlobal (1); private readonly CursesDriver _cursesDriver; private readonly Dictionary _descriptorWatchers = new (); private readonly int [] _wakeUpPipes = new int [2]; - private MainLoop _mainLoop; + private MainLoop? _mainLoop; private bool _pollDirty = true; - private Pollfd [] _pollMap; + private Pollfd []? _pollMap; private bool _winChanged; - public UnixMainLoop (ConsoleDriver consoleDriver = null) + public UnixMainLoop (IConsoleDriver IConsoleDriver) { - // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does. - _cursesDriver = (CursesDriver)Application.Driver; + ArgumentNullException.ThrowIfNull (IConsoleDriver); + + _cursesDriver = (CursesDriver)IConsoleDriver; } void IMainLoopDriver.Wakeup () @@ -77,7 +79,7 @@ void IMainLoopDriver.Setup (MainLoop mainLoop) AddWatch ( _wakeUpPipes [0], Condition.PollIn, - ml => + _ => { read (_wakeUpPipes [0], _ignore, 1); @@ -93,11 +95,16 @@ void IMainLoopDriver.Setup (MainLoop mainLoop) bool IMainLoopDriver.EventsPending () { + if (ConsoleDriver.RunningUnitTests) + { + return true; + } + UpdatePollMap (); - bool checkTimersResult = _mainLoop.CheckTimersAndIdleHandlers (out int pollTimeout); + bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout); - int n = poll (_pollMap, (uint)_pollMap.Length, pollTimeout); + int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout); if (n == KEY_RESIZE) { @@ -109,6 +116,11 @@ bool IMainLoopDriver.EventsPending () void IMainLoopDriver.Iteration () { + if (ConsoleDriver.RunningUnitTests) + { + return; + } + if (_winChanged) { _winChanged = false; @@ -125,19 +137,17 @@ void IMainLoopDriver.Iteration () foreach (Pollfd p in _pollMap) { - Watch watch; - if (p.revents == 0) { continue; } - if (!_descriptorWatchers.TryGetValue (p.fd, out watch)) + if (!_descriptorWatchers.TryGetValue (p.fd, out Watch? watch)) { continue; } - if (!watch.Callback (_mainLoop)) + if (!watch.Callback (_mainLoop!)) { _descriptorWatchers.Remove (p.fd); } @@ -146,7 +156,7 @@ void IMainLoopDriver.Iteration () void IMainLoopDriver.TearDown () { - _descriptorWatchers?.Clear (); + _descriptorWatchers.Clear (); _mainLoop = null; } @@ -159,10 +169,7 @@ void IMainLoopDriver.TearDown () /// internal object AddWatch (int fileDescriptor, Condition condition, Func callback) { - if (callback is null) - { - throw new ArgumentNullException (nameof (callback)); - } + ArgumentNullException.ThrowIfNull (callback); var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor }; _descriptorWatchers [fileDescriptor] = watch; @@ -186,15 +193,6 @@ internal void RemoveWatch (object token) } } - [DllImport ("libc")] - private static extern int pipe ([In] [Out] int [] pipes); - - [DllImport ("libc")] - private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout); - - [DllImport ("libc")] - private static extern int read (int fd, nint buf, nint n); - private void UpdatePollMap () { if (!_pollDirty) @@ -215,9 +213,30 @@ private void UpdatePollMap () } } + internal void WriteRaw (string ansiRequest) + { + // Write to stdout (fd 1) + write (STDOUT_FILENO, ansiRequest, ansiRequest.Length); + } + + [DllImport ("libc")] + private static extern int pipe ([In][Out] int [] pipes); + + [DllImport ("libc")] + private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout); + + [DllImport ("libc")] + private static extern int read (int fd, nint buf, nint n); + [DllImport ("libc")] private static extern int write (int fd, nint buf, nint n); + // File descriptor for stdout + private const int STDOUT_FILENO = 1; + + [DllImport ("libc")] + private static extern int write (int fd, string buf, int n); + [StructLayout (LayoutKind.Sequential)] private struct Pollfd { diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs deleted file mode 100644 index 29ef5afa79..0000000000 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs +++ /dev/null @@ -1,125 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Represents the status of an ANSI escape sequence request made to the terminal using -/// . -/// -/// -public class EscSeqReqStatus -{ - /// Creates a new state of escape sequence request. - /// The terminator. - /// The number of requests. - public EscSeqReqStatus (string terminator, int numReq) - { - Terminator = terminator; - NumRequests = NumOutstanding = numReq; - } - - /// Gets the number of unfinished requests. - public int NumOutstanding { get; set; } - - /// Gets the number of requests. - public int NumRequests { get; } - - /// Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator). - public string Terminator { get; } -} - -// TODO: This class is a singleton. It should use the singleton pattern. -/// -/// Manages ANSI Escape Sequence requests and responses. The list of contains the -/// status of the request. Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator). -/// -public class EscSeqRequests -{ - /// Gets the list. - public List Statuses { get; } = new (); - - /// - /// Adds a new request for the ANSI Escape Sequence defined by . Adds a - /// instance to list. - /// - /// The terminator. - /// The number of requests. - public void Add (string terminator, int numReq = 1) - { - lock (Statuses) - { - EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator); - - if (found is null) - { - Statuses.Add (new EscSeqReqStatus (terminator, numReq)); - } - else if (found is { } && found.NumOutstanding < found.NumRequests) - { - found.NumOutstanding = Math.Min (found.NumOutstanding + numReq, found.NumRequests); - } - } - } - - /// - /// Indicates if a with the exists in the - /// list. - /// - /// - /// if exist, otherwise. - public bool HasResponse (string terminator) - { - lock (Statuses) - { - EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator); - - if (found is null) - { - return false; - } - - if (found is { NumOutstanding: > 0 }) - { - return true; - } - - // BUGBUG: Why does an API that returns a bool remove the entry from the list? - // NetDriver and Unit tests never exercise this line of code. Maybe Curses does? - Statuses.Remove (found); - - return false; - } - } - - /// - /// Removes a request defined by . If a matching is - /// found and the number of outstanding requests is greater than 0, the number of outstanding requests is decremented. - /// If the number of outstanding requests is 0, the is removed from - /// . - /// - /// The terminating string. - public void Remove (string terminator) - { - lock (Statuses) - { - EscSeqReqStatus found = Statuses.Find (x => x.Terminator == terminator); - - if (found is null) - { - return; - } - - if (found is { } && found.NumOutstanding == 0) - { - Statuses.Remove (found); - } - else if (found is { } && found.NumOutstanding > 0) - { - found.NumOutstanding--; - - if (found.NumOutstanding == 0) - { - Statuses.Remove (found); - } - } - } - } -} diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs new file mode 100644 index 0000000000..d832f06594 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReqStatus.cs @@ -0,0 +1,28 @@ +#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 terminator. + /// The number of requests. + public EscSeqReqStatus (string terminator, int numReq) + { + Terminator = terminator; + NumRequests = NumOutstanding = numReq; + } + + /// Gets the number of unfinished requests. + public int NumOutstanding { get; internal set; } + + /// Gets the number of requests. + public int NumRequests { get; internal set; } + + /// Gets the Escape Sequence Terminator (e.g. ESC[8t ... t is the terminator). + public string Terminator { get; } +} diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqRequests.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqRequests.cs new file mode 100644 index 0000000000..579aa0cd95 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqRequests.cs @@ -0,0 +1,98 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Manages ANSI Escape Sequence requests and responses. The list of +/// contains the +/// status of the request. Each request is identified by the terminator (e.g. ESC[8t ... t is the terminator). +/// +public static class EscSeqRequests +{ + /// Gets the list. + public static List Statuses { get; } = []; + + /// + /// Adds a new request for the ANSI Escape Sequence defined by . Adds a + /// instance to list. + /// + /// The terminator. + /// The number of requests. + public static void Add (string terminator, int numRequests = 1) + { + ArgumentException.ThrowIfNullOrEmpty (terminator); + + int numReq = Math.Max (numRequests, 1); + + lock (Statuses) + { + EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator); + + if (found is null) + { + Statuses.Add (new (terminator, numReq)); + } + else + { + found.NumRequests += numReq; + found.NumOutstanding += numReq; + } + } + } + + /// + /// Clear the property. + /// + public static void Clear () + { + lock (Statuses) + { + Statuses.Clear (); + } + } + + /// + /// Indicates if a with the exists in the + /// list. + /// + /// + /// if exist, otherwise. + public static bool HasResponse (string terminator) + { + lock (Statuses) + { + EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator); + + return found is { }; + } + } + + /// + /// Removes a request defined by . If a matching is + /// found and the number of outstanding requests is greater than 0, the number of outstanding requests is decremented. + /// If the number of outstanding requests is 0, the is removed from + /// . + /// + /// The terminating string. + public static void Remove (string terminator) + { + ArgumentException.ThrowIfNullOrEmpty (terminator); + + lock (Statuses) + { + EscSeqReqStatus? found = Statuses.Find (x => x.Terminator == terminator); + + if (found is null) + { + return; + } + + found.NumOutstanding--; + + if (found.NumOutstanding == 0) + { + Statuses.Remove (found); + } + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 6330c33702..709f97f5f3 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1,5 +1,16 @@ +#nullable enable +using Terminal.Gui.ConsoleDrivers; +using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; + namespace Terminal.Gui; +// QUESTION: Should this class be refactored into separate classes for: +// QUESTION: CSI definitions +// QUESTION: Primitives like DecodeEsqReq +// QUESTION: Screen/Color/Cursor handling +// QUESTION: Mouse handling +// QUESTION: Keyboard handling + /// /// Provides a platform-independent API for managing ANSI escape sequences. /// @@ -11,6 +22,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. /// @@ -37,6 +49,9 @@ public enum ClearScreenOptions EntireScreenAndScrollbackBuffer = 3 } + // QUESTION: I wonder if EscSeqUtils.CSI_... should be more strongly typed such that this (and Terminator could be + // QUESTION: public required CSIRequests Request { get; init; } + // QUESTION: public required CSITerminators Terminator { get; init; } /// /// Escape key code (ASCII 27/0x1B). /// @@ -122,19 +137,19 @@ public enum ClearScreenOptions public static readonly string CSI_SaveCursorAndActivateAltBufferNoBackscroll = CSI + "?1049h"; //private static bool isButtonReleased; - private static bool isButtonClicked; + private static bool _isButtonClicked; - private static bool isButtonDoubleClicked; + private static bool _isButtonDoubleClicked; //private static MouseFlags? lastMouseButtonReleased; // QUESTION: What's the difference between isButtonClicked and isButtonPressed? // Some clarity or comments would be handy, here. // It also seems like some enforcement of valid states might be a good idea. - private static bool isButtonPressed; - private static bool isButtonTripleClicked; + private static bool _isButtonPressed; + private static bool _isButtonTripleClicked; - private static MouseFlags? lastMouseButtonPressed; - private static Point? point; + private static MouseFlags? _lastMouseButtonPressed; + private static Point? _point; /// /// Control sequence for disabling mouse events. @@ -155,10 +170,14 @@ public enum ClearScreenOptions /// public static string CSI_ClearScreen (ClearScreenOptions option) { return $"{CSI}{(int)option}J"; } + /// + /// Specify the incomplete array not yet recognized as valid ANSI escape sequence. + /// + public static ConsoleKeyInfo []? IncompleteCkInfos { get; set; } + /// /// Decodes an ANSI escape sequence. /// - /// The which may contain a request. /// The which may change. /// The which may change. /// The array. @@ -173,7 +192,6 @@ public enum ClearScreenOptions /// Indicates if the escape sequence is a response to a request. /// The handler that will process the event. public static void DecodeEscSeq ( - EscSeqRequests escSeqRequests, ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ConsoleKeyInfo [] cki, @@ -186,16 +204,16 @@ public static void DecodeEscSeq ( out List buttonState, out Point pos, out bool isResponse, - Action continuousButtonPressedHandler + Action? continuousButtonPressedHandler ) { char [] kChars = GetKeyCharArray (cki); (c1Control, code, values, terminator) = GetEscapeResult (kChars); isMouse = false; - buttonState = new List { 0 }; + buttonState = [0]; pos = default (Point); isResponse = false; - char keyChar = '\0'; + var keyChar = '\0'; switch (c1Control) { @@ -204,56 +222,121 @@ Action continuousButtonPressedHandler { key = ConsoleKey.Escape; - newConsoleKeyInfo = new ConsoleKeyInfo ( - cki [0].KeyChar, - key, - (mod & ConsoleModifiers.Shift) != 0, - (mod & ConsoleModifiers.Alt) != 0, - (mod & ConsoleModifiers.Control) != 0); + newConsoleKeyInfo = new ( + cki [0].KeyChar, + key, + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); } - else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26) + else if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26 && (uint)cki [1].KeyChar != '\n' && (uint)cki [1].KeyChar != '\r') { key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1); + mod = ConsoleModifiers.Alt | ConsoleModifiers.Control; + + newConsoleKeyInfo = new ( + cki [1].KeyChar, + key, + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); + } + else if (cki [1].KeyChar >= 65 && cki [1].KeyChar <= 90) + { + key = (ConsoleKey)cki [1].KeyChar; + mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt; + + newConsoleKeyInfo = new ( + cki [1].KeyChar, + (ConsoleKey)Math.Min ((uint)key, 255), + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); + } + else if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122) + { + key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0]; + mod = ConsoleModifiers.Alt; + + newConsoleKeyInfo = new ( + cki [1].KeyChar, + (ConsoleKey)Math.Min ((uint)key, 255), + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); + } + else if (cki [1].KeyChar is '\0' or ' ') + { + key = ConsoleKey.Spacebar; - newConsoleKeyInfo = new ConsoleKeyInfo ( - cki [1].KeyChar, - key, - false, - true, - true); + if (kChars.Length > 1 && kChars [1] == '\0') + { + mod = ConsoleModifiers.Alt | ConsoleModifiers.Control; + } + else + { + mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt; + } + + newConsoleKeyInfo = new ( + cki [1].KeyChar, + (ConsoleKey)Math.Min ((uint)key, 255), + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); } - else + else if (cki [1].KeyChar is '\n' or '\r') { - if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122) + key = ConsoleKey.Enter; + + if (kChars.Length > 1 && kChars [1] == '\n') { - key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0]; + mod = ConsoleModifiers.Alt | ConsoleModifiers.Control; } else { - key = (ConsoleKey)cki [1].KeyChar; + mod = ConsoleModifiers.Shift | ConsoleModifiers.Alt; } - newConsoleKeyInfo = new ConsoleKeyInfo ( - (char)key, - (ConsoleKey)Math.Min ((uint)key, 255), - false, - true, - false); + newConsoleKeyInfo = new ( + cki [1].KeyChar, + (ConsoleKey)Math.Min ((uint)key, 255), + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); + } + else + { + key = (ConsoleKey)cki [1].KeyChar; + mod = ConsoleModifiers.Alt; + + newConsoleKeyInfo = new ( + cki [1].KeyChar, + (ConsoleKey)Math.Min ((uint)key, 255), + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); } break; case "SS3": key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar); - newConsoleKeyInfo = new ConsoleKeyInfo ( - keyChar, - key, - (mod & ConsoleModifiers.Shift) != 0, - (mod & ConsoleModifiers.Alt) != 0, - (mod & ConsoleModifiers.Control) != 0); + newConsoleKeyInfo = new ( + keyChar, + key, + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); break; case "CSI": + // Reset always IncompleteCkInfos + if (IncompleteCkInfos is { }) + { + IncompleteCkInfos = null; + } + if (!string.IsNullOrEmpty (code) && code == "<") { GetMouse (cki, out buttonState, out pos, continuousButtonPressedHandler); @@ -262,16 +345,18 @@ Action continuousButtonPressedHandler return; } - if (escSeqRequests is { } && escSeqRequests.HasResponse (terminator)) + if (EscSeqRequests.HasResponse (terminator)) { isResponse = true; - escSeqRequests.Remove (terminator); + EscSeqRequests.Remove (terminator); return; } if (!string.IsNullOrEmpty (terminator)) { + System.Diagnostics.Debug.Assert (terminator.Length == 1); + key = GetConsoleKey (terminator [0], values [0], ref mod, ref keyChar); if (key != 0 && values.Length > 1) @@ -279,31 +364,43 @@ Action continuousButtonPressedHandler mod |= GetConsoleModifiers (values [1]); } - newConsoleKeyInfo = new ConsoleKeyInfo ( - keyChar, - key, - (mod & ConsoleModifiers.Shift) != 0, - (mod & ConsoleModifiers.Alt) != 0, - (mod & ConsoleModifiers.Control) != 0); + if (keyChar != 0 || key != 0 || mod != 0) + { + newConsoleKeyInfo = new ( + keyChar, + key, + (mod & ConsoleModifiers.Shift) != 0, + (mod & ConsoleModifiers.Alt) != 0, + (mod & ConsoleModifiers.Control) != 0); + } + else + { + // It's request response that wasn't handled by a valid request terminator + System.Diagnostics.Debug.Assert (EscSeqRequests.Statuses.Count > 0); + + isResponse = true; + EscSeqRequests.Remove (terminator); + } } else { // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/2803 // This is caused by NetDriver depending on Console.KeyAvailable? - throw new InvalidOperationException ("CSI response, but there's no terminator"); + //throw new InvalidOperationException ("CSI response, but there's no terminator"); - //newConsoleKeyInfo = new ConsoleKeyInfo ('\0', - // key, - // (mod & ConsoleModifiers.Shift) != 0, - // (mod & ConsoleModifiers.Alt) != 0, - // (mod & ConsoleModifiers.Control) != 0); + IncompleteCkInfos = cki; } + break; + default: + newConsoleKeyInfo = MapConsoleKeyInfo (cki [0]); + key = newConsoleKeyInfo.Key; + mod = newConsoleKeyInfo.Modifiers; + break; } } - #nullable enable /// /// Gets the c1Control used in the called escape sequence. /// @@ -335,6 +432,7 @@ public static string GetC1ControlChar (in char c) }; } + /// /// Gets the depending on terminating and value. /// @@ -369,6 +467,7 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons ('B', _) => ConsoleKey.DownArrow, ('C', _) => ConsoleKey.RightArrow, ('D', _) => ConsoleKey.LeftArrow, + ('E', _) => ConsoleKey.Clear, ('F', _) => ConsoleKey.End, ('H', _) => ConsoleKey.Home, ('P', _) => ConsoleKey.F1, @@ -388,18 +487,19 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons ('~', "21") => ConsoleKey.F10, ('~', "23") => ConsoleKey.F11, ('~', "24") => ConsoleKey.F12, - ('l', _) => ConsoleKey.Add, - ('m', _) => ConsoleKey.Subtract, - ('p', _) => ConsoleKey.Insert, - ('q', _) => ConsoleKey.End, - ('r', _) => ConsoleKey.DownArrow, - ('s', _) => ConsoleKey.PageDown, - ('t', _) => ConsoleKey.LeftArrow, - ('u', _) => ConsoleKey.Clear, - ('v', _) => ConsoleKey.RightArrow, - ('w', _) => ConsoleKey.Home, - ('x', _) => ConsoleKey.UpArrow, - ('y', _) => ConsoleKey.PageUp, + // These terminators are used by macOS on a numeric keypad without keys modifiers + ('l', null) => ConsoleKey.Add, + ('m', null) => ConsoleKey.Subtract, + ('p', null) => ConsoleKey.Insert, + ('q', null) => ConsoleKey.End, + ('r', null) => ConsoleKey.DownArrow, + ('s', null) => ConsoleKey.PageDown, + ('t', null) => ConsoleKey.LeftArrow, + ('u', null) => ConsoleKey.Clear, + ('v', null) => ConsoleKey.RightArrow, + ('w', null) => ConsoleKey.Home, + ('x', null) => ConsoleKey.UpArrow, + ('y', null) => ConsoleKey.PageUp, (_, _) => 0 }; } @@ -434,7 +534,7 @@ public static ConsoleModifiers GetConsoleModifiers (string? value) /// public static (string c1Control, string code, string [] values, string terminating) GetEscapeResult (char [] kChar) { - if (kChar is null || kChar.Length == 0) + if (kChar is null || kChar.Length == 0 || (kChar.Length == 1 && kChar [0] != KeyEsc)) { return (null, null, null, null); } @@ -497,7 +597,7 @@ public static (string c1Control, string code, string [] values, string terminati // PERF: This is expensive public static char [] GetKeyCharArray (ConsoleKeyInfo [] cki) { - char [] kChar = { }; + char [] kChar = []; var length = 0; foreach (ConsoleKeyInfo kc in cki) @@ -774,36 +874,36 @@ Action continuousButtonPressedHandler mouseFlags = [MouseFlags.AllEvents]; - if (lastMouseButtonPressed != null - && !isButtonPressed + if (_lastMouseButtonPressed != null + && !_isButtonPressed && !buttonState.HasFlag (MouseFlags.ReportMousePosition) && !buttonState.HasFlag (MouseFlags.Button1Released) && !buttonState.HasFlag (MouseFlags.Button2Released) && !buttonState.HasFlag (MouseFlags.Button3Released) && !buttonState.HasFlag (MouseFlags.Button4Released)) { - lastMouseButtonPressed = null; - isButtonPressed = false; + _lastMouseButtonPressed = null; + _isButtonPressed = false; } - if ((!isButtonClicked - && !isButtonDoubleClicked + if ((!_isButtonClicked + && !_isButtonDoubleClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed) - && lastMouseButtonPressed is null) - || (isButtonPressed && lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition))) + && _lastMouseButtonPressed is null) + || (_isButtonPressed && _lastMouseButtonPressed is { } && buttonState.HasFlag (MouseFlags.ReportMousePosition))) { mouseFlags [0] = buttonState; - lastMouseButtonPressed = buttonState; - isButtonPressed = true; + _lastMouseButtonPressed = buttonState; + _isButtonPressed = true; - point = pos; + _point = pos; if ((mouseFlags [0] & MouseFlags.ReportMousePosition) == 0) { - Application.MainLoop.AddIdle ( + Application.MainLoop?.AddIdle ( () => { // INTENT: What's this trying to do? @@ -818,7 +918,7 @@ Action continuousButtonPressedHandler } else if (mouseFlags [0].HasFlag (MouseFlags.ReportMousePosition)) { - point = pos; + _point = pos; // The isButtonPressed must always be true, otherwise we can lose the feature // If mouse flags has ReportMousePosition this feature won't run @@ -826,27 +926,27 @@ Action continuousButtonPressedHandler //isButtonPressed = false; } } - else if (isButtonDoubleClicked + else if (_isButtonDoubleClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) { mouseFlags [0] = GetButtonTripleClicked (buttonState); - isButtonDoubleClicked = false; - isButtonTripleClicked = true; + _isButtonDoubleClicked = false; + _isButtonTripleClicked = true; } - else if (isButtonClicked + else if (_isButtonClicked && (buttonState == MouseFlags.Button1Pressed || buttonState == MouseFlags.Button2Pressed || buttonState == MouseFlags.Button3Pressed || buttonState == MouseFlags.Button4Pressed)) { mouseFlags [0] = GetButtonDoubleClicked (buttonState); - isButtonClicked = false; - isButtonDoubleClicked = true; + _isButtonClicked = false; + _isButtonDoubleClicked = true; - Application.MainLoop.AddIdle ( + Application.MainLoop?.AddIdle ( () => { Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); @@ -866,26 +966,26 @@ Action continuousButtonPressedHandler // }); //} - else if (!isButtonClicked - && !isButtonDoubleClicked + else if (!_isButtonClicked + && !_isButtonDoubleClicked && (buttonState == MouseFlags.Button1Released || buttonState == MouseFlags.Button2Released || buttonState == MouseFlags.Button3Released || buttonState == MouseFlags.Button4Released)) { mouseFlags [0] = buttonState; - isButtonPressed = false; + _isButtonPressed = false; - if (isButtonTripleClicked) + if (_isButtonTripleClicked) { - isButtonTripleClicked = false; + _isButtonTripleClicked = false; } - else if (pos.X == point?.X && pos.Y == point?.Y) + else if (pos.X == _point?.X && pos.Y == _point?.Y) { mouseFlags.Add (GetButtonClicked (buttonState)); - isButtonClicked = true; + _isButtonClicked = true; - Application.MainLoop.AddIdle ( + Application.MainLoop?.AddIdle ( () => { Task.Run (async () => await ProcessButtonClickedAsync ()); @@ -894,7 +994,7 @@ Action continuousButtonPressedHandler }); } - point = pos; + _point = pos; //if ((lastMouseButtonPressed & MouseFlags.ReportMousePosition) == 0) { // lastMouseButtonReleased = buttonState; @@ -952,7 +1052,7 @@ Action continuousButtonPressedHandler public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) { ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo; - ConsoleKey key; + ConsoleKey key = ConsoleKey.None; char keyChar = consoleKeyInfo.KeyChar; switch ((uint)keyChar) @@ -960,56 +1060,176 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) case 0: if (consoleKeyInfo.Key == (ConsoleKey)64) { // Ctrl+Space in Windows. - newConsoleKeyInfo = new ConsoleKeyInfo ( - ' ', - ConsoleKey.Spacebar, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + ConsoleKey.Spacebar, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + } + else if (consoleKeyInfo.Key == ConsoleKey.None) + { + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + ConsoleKey.Spacebar, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + true); } break; - case uint n when n > 0 && n <= KeyEsc: - if (consoleKeyInfo.Key == 0 && consoleKeyInfo.KeyChar == '\r') + case uint n when n is > 0 and <= KeyEsc: + if (consoleKeyInfo is { Key: 0, KeyChar: '\u001B' }) + { + key = ConsoleKey.Escape; + + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + } + else if (consoleKeyInfo is { Key: 0, KeyChar: '\t' }) + { + key = ConsoleKey.Tab; + + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + } + else if (consoleKeyInfo is { Key: 0, KeyChar: '\r' }) { key = ConsoleKey.Enter; - newConsoleKeyInfo = new ConsoleKeyInfo ( - consoleKeyInfo.KeyChar, - key, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + } + else if (consoleKeyInfo is { Key: 0, KeyChar: '\n' }) + { + key = ConsoleKey.Enter; + + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + true); } else if (consoleKeyInfo.Key == 0) { key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + (uint)ConsoleKey.A - 1); - newConsoleKeyInfo = new ConsoleKeyInfo ( - (char)key, - key, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, - true); + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + true); } break; case 127: // DEL - newConsoleKeyInfo = new ConsoleKeyInfo ( - consoleKeyInfo.KeyChar, - ConsoleKey.Backspace, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, - (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); + key = ConsoleKey.Backspace; + + newConsoleKeyInfo = new ( + consoleKeyInfo.KeyChar, + key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); break; default: - newConsoleKeyInfo = consoleKeyInfo; + uint ck = MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey); + + if (isConsoleKey) + { + key = (ConsoleKey)ck; + } + + newConsoleKeyInfo = new ( + keyChar, + key, + GetShiftMod (consoleKeyInfo.Modifiers), + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); break; } return newConsoleKeyInfo; + + bool GetShiftMod (ConsoleModifiers modifiers) + { + if (consoleKeyInfo.KeyChar is >= (char)ConsoleKey.A and <= (char)ConsoleKey.Z && modifiers == ConsoleModifiers.None) + { + return true; + } + + return (modifiers & ConsoleModifiers.Shift) != 0; + } + } + + private static MouseFlags _lastMouseFlags; + + /// + /// Provides a handler to be invoked when mouse continuous button pressed is processed. + /// + public static event EventHandler ContinuousButtonPressed; + + /// + /// Provides a default mouse event handler that can be used by any driver. + /// + /// The mouse flags event. + /// The mouse position. + public static void ProcessMouseEvent (MouseFlags mouseFlag, Point pos) + { + bool WasButtonReleased (MouseFlags flag) + { + return flag.HasFlag (MouseFlags.Button1Released) + || flag.HasFlag (MouseFlags.Button2Released) + || flag.HasFlag (MouseFlags.Button3Released) + || flag.HasFlag (MouseFlags.Button4Released); + } + + bool IsButtonNotPressed (MouseFlags flag) + { + return !flag.HasFlag (MouseFlags.Button1Pressed) + && !flag.HasFlag (MouseFlags.Button2Pressed) + && !flag.HasFlag (MouseFlags.Button3Pressed) + && !flag.HasFlag (MouseFlags.Button4Pressed); + } + + bool IsButtonClickedOrDoubleClicked (MouseFlags flag) + { + return flag.HasFlag (MouseFlags.Button1Clicked) + || flag.HasFlag (MouseFlags.Button2Clicked) + || flag.HasFlag (MouseFlags.Button3Clicked) + || flag.HasFlag (MouseFlags.Button4Clicked) + || flag.HasFlag (MouseFlags.Button1DoubleClicked) + || flag.HasFlag (MouseFlags.Button2DoubleClicked) + || flag.HasFlag (MouseFlags.Button3DoubleClicked) + || flag.HasFlag (MouseFlags.Button4DoubleClicked); + } + + if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0)) + { + return; + } + + _lastMouseFlags = mouseFlag; + + var me = new MouseEventArgs { Flags = mouseFlag, Position = pos }; + + ContinuousButtonPressed?.Invoke ((mouseFlag, pos), me); } /// @@ -1021,7 +1241,61 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) public static ConsoleKeyInfo [] ResizeArray (ConsoleKeyInfo consoleKeyInfo, ConsoleKeyInfo [] cki) { Array.Resize (ref cki, cki is null ? 1 : cki.Length + 1); - cki [cki.Length - 1] = consoleKeyInfo; + cki [^1] = consoleKeyInfo; + + return cki; + } + + /// + /// Insert a array into the another array at the specified + /// index. + /// + /// The array to insert. + /// The array where will be added the array. + /// The start index to insert the array, default is 0. + /// The array with another array inserted. + public static ConsoleKeyInfo [] InsertArray ([CanBeNull] ConsoleKeyInfo [] toInsert, ConsoleKeyInfo [] cki, int index = 0) + { + if (toInsert is null) + { + return cki; + } + + if (cki is null) + { + return toInsert; + } + + if (index < 0) + { + index = 0; + } + + ConsoleKeyInfo [] backupCki = cki.Clone () as ConsoleKeyInfo []; + + Array.Resize (ref cki, cki.Length + toInsert.Length); + + for (var i = 0; i < cki.Length; i++) + { + if (i == index) + { + for (var j = 0; j < toInsert.Length; j++) + { + cki [i] = toInsert [j]; + i++; + } + + for (int k = index; k < backupCki!.Length; k++) + { + cki [i] = backupCki [k]; + i++; + } + } + else + { + cki [i] = backupCki! [i]; + } + } return cki; } @@ -1101,16 +1375,117 @@ private static MouseFlags GetButtonTripleClicked (MouseFlags mouseFlag) return mf; } + internal static KeyCode MapKey (ConsoleKeyInfo keyInfo) + { + switch (keyInfo.Key) + { + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + case ConsoleKey.Packet: + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + if (keyInfo.KeyChar == 0) + { + // If the keyChar is 0, keyInfo.Key value is not a printable character. + System.Diagnostics.Debug.Assert (keyInfo.Key == 0); + + return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); + } + + if (keyInfo.Modifiers != ConsoleModifiers.Shift) + { + // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + + // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") + // and passing on Shift would be redundant. + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); + } + + // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC + if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) + { + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I) + { + return KeyCode.Tab; + } + + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); + } + + // Handle control keys (e.g. CursorUp) + if (keyInfo.Key != ConsoleKey.None + && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); + } + + if ((ConsoleKey)keyInfo.KeyChar is >= ConsoleKey.A and <= ConsoleKey.Z) + { + // Shifted + keyInfo = new ( + keyInfo.KeyChar, + (ConsoleKey)keyInfo.KeyChar, + true, + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); + } + + if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) + { + // Unshifted + keyInfo = new ( + keyInfo.KeyChar, + (ConsoleKey)(keyInfo.KeyChar - 32), + false, + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); + } + + if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) + { + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) + || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); + } + + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + { + // If ShiftMask is on add the ShiftMask + if (char.IsUpper (keyInfo.KeyChar)) + { + return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; + } + } + + return (KeyCode)keyInfo.Key; + } + + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + private static async Task ProcessButtonClickedAsync () { await Task.Delay (300); - isButtonClicked = false; + _isButtonClicked = false; } private static async Task ProcessButtonDoubleClickedAsync () { await Task.Delay (300); - isButtonDoubleClicked = false; + _isButtonDoubleClicked = false; } private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag, Action continuousButtonPressedHandler) @@ -1118,7 +1493,7 @@ private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseF // PERF: Pause and poll in a hot loop. // This should be replaced with event dispatch and a synchronization primitive such as AutoResetEvent. // Will make a massive difference in responsiveness. - while (isButtonPressed) + while (_isButtonPressed) { await Task.Delay (100); @@ -1129,9 +1504,9 @@ private static async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseF break; } - if (isButtonPressed && lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0) + if (_isButtonPressed && _lastMouseButtonPressed is { } && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { - Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, point ?? Point.Empty)); + Application.Invoke (() => continuousButtonPressedHandler (mouseFlag, _point ?? Point.Empty)); } } } @@ -1156,6 +1531,112 @@ private static MouseFlags SetControlKeyStates (MouseFlags buttonState, MouseFlag return mouseFlag; } + /// + /// Split a raw string into a list of string with the correct ansi escape sequence. + /// + /// The raw string containing one or many ansi escape sequence. + /// A list with a valid ansi escape sequence. + public static List SplitEscapeRawString (string rawData) + { + List splitList = []; + var isEscSeq = false; + var split = string.Empty; + char previousChar = '\0'; + + for (var i = 0; i < rawData.Length; i++) + { + char c = rawData [i]; + + if (c == '\u001B') + { + isEscSeq = true; + + split = AddAndClearSplit (); + + split += c.ToString (); + } + else if (!isEscSeq && c >= Key.Space) + { + split = AddAndClearSplit (); + splitList.Add (c.ToString ()); + } + else if ((previousChar != '\u001B' && c <= Key.Space) || (previousChar != '\u001B' && c == 127) + || (char.IsLetter (previousChar) && char.IsLower (c) && char.IsLetter (c)) + || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsLetterOrDigit (c)) + || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsPunctuation (c)) + || (!string.IsNullOrEmpty (split) && split.Length > 2 && char.IsLetter (previousChar) && char.IsSymbol (c))) + { + isEscSeq = false; + split = AddAndClearSplit (); + splitList.Add (c.ToString ()); + } + else + { + split += c.ToString (); + } + + if (!string.IsNullOrEmpty (split) && i == rawData.Length - 1) + { + splitList.Add (split); + } + + previousChar = c; + } + + return splitList; + + string AddAndClearSplit () + { + if (!string.IsNullOrEmpty (split)) + { + splitList.Add (split); + split = string.Empty; + } + + return split; + } + } + + /// + /// Convert a array to string. + /// + /// + /// The string representing the array. + public static string ToString (ConsoleKeyInfo [] consoleKeyInfos) + { + StringBuilder sb = new (); + + foreach (ConsoleKeyInfo keyChar in consoleKeyInfos) + { + sb.Append (keyChar.KeyChar); + } + + return sb.ToString (); + } + + /// + /// Convert a string to array. + /// + /// + /// The representing the string. + public static ConsoleKeyInfo [] ToConsoleKeyInfoArray (string ansi) + { + if (ansi is null) + { + return null; + } + + ConsoleKeyInfo [] cki = new ConsoleKeyInfo [ansi.Length]; + + for (var i = 0; i < ansi.Length; i++) + { + char c = ansi [i]; + cki [i] = new (c, 0, false, false, false); + } + + return cki; + } + #region Cursor //ESC [ M - RI Reverse Index – Performs the reverse operation of \n, moves cursor up one line, maintains horizontal position, scrolls buffer if necessary* @@ -1316,13 +1797,14 @@ public enum DECSCUSR_Style /// /// ESC [ ? 6 n - Request Cursor Position Report (?) (DECXCPR) /// https://terminalguide.namepad.de/seq/csi_sn__p-6/ + /// The terminal reply to . ESC [ ? (y) ; (x) ; 1 R /// public static readonly string CSI_RequestCursorPositionReport = CSI + "?6n"; /// /// The terminal reply to . ESC [ ? (y) ; (x) R /// - public const string CSI_RequestCursorPositionReport_Terminator = "R"; + public static readonly string CSI_RequestCursorPositionReport_Terminator = "R"; /// /// ESC [ 0 c - Send Device Attributes (Primary DA) @@ -1341,20 +1823,18 @@ public enum DECSCUSR_Style /// 28 = Rectangular area operations /// 32 = Text macros /// 42 = ISO Latin-2 character set + /// The terminator indicating a reply to or + /// /// public static readonly string CSI_SendDeviceAttributes = CSI + "0c"; /// /// ESC [ > 0 c - Send Device Attributes (Secondary DA) /// Windows Terminal v1.18+ emits: "\x1b[>0;10;1c" (vt100, firmware version 1.0, vt220) - /// - public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c"; - - /// /// The terminator indicating a reply to or /// /// - public const string CSI_ReportDeviceAttributes_Terminator = "c"; + public static readonly string CSI_SendDeviceAttributes2 = CSI + ">0c"; /* TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768 @@ -1372,19 +1852,20 @@ public enum DECSCUSR_Style /// /// CSI 1 8 t | yes | yes | yes | report window size in chars /// https://terminalguide.namepad.de/seq/csi_st-18/ + /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// public static readonly string CSI_ReportTerminalSizeInChars = CSI + "18t"; /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// - public const string CSI_ReportTerminalSizeInChars_Terminator = "t"; + public static readonly string CSI_ReportTerminalSizeInChars_Terminator = "t"; /// /// The value of the response to indicating value 1 and 2 are the terminal /// size in chars. /// - public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; + public static readonly string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; #endregion } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index a5f297ee69..a9117cee44 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -76,7 +76,7 @@ public FakeDriver () } } - internal override void End () + public override void End () { FakeConsole.ResetColor (); FakeConsole.Clear (); @@ -84,7 +84,7 @@ internal override void End () private FakeMainLoop _mainLoopDriver; - internal override MainLoop Init () + public override MainLoop Init () { FakeConsole.MockKeyPresses.Clear (); @@ -392,6 +392,9 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); } + /// + public override void WriteRaw (string ansi) { throw new NotImplementedException (); } + public void SetBufferSize (int width, int height) { FakeConsole.SetBufferSize (width, height); diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs index d90caace7b..6b6789fc46 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs @@ -4,7 +4,7 @@ internal class FakeMainLoop : IMainLoopDriver { public Action MockKeyPressed; - public FakeMainLoop (ConsoleDriver consoleDriver = null) + public FakeMainLoop (IConsoleDriver consoleDriver = null) { // No implementation needed for FakeMainLoop } diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs new file mode 100644 index 0000000000..d5e9cd009d --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -0,0 +1,295 @@ +#nullable enable +namespace Terminal.Gui; + +/// Base interface for Terminal.Gui ConsoleDriver implementations. +/// +/// There are currently four implementations: - (for Unix and Mac) - +/// - that uses the .NET Console API - +/// for unit testing. +/// +public interface IConsoleDriver +{ + /// Get the operating system clipboard. + IClipboard? Clipboard { get; } + + /// Gets the location and size of the terminal screen. + Rectangle Screen { get; } + + /// + /// Gets or sets the clip rectangle that and are subject + /// to. + /// + /// The rectangle describing the of region. + Region? Clip { get; set; } + + /// + /// Gets the column last set by . and are used by + /// and to determine where to add content. + /// + int Col { get; } + + /// The number of columns visible in the terminal. + int Cols { get; set; } + + /// + /// The contents of the application output. The driver outputs this buffer to the terminal when + /// is called. + /// The format of the array is rows, columns. The first index is the row, the second index is the column. + /// + Cell [,]? Contents { get; set; } + + /// The leftmost column in the terminal. + int Left { get; set; } + + /// + /// Gets the row last set by . and are used by + /// and to determine where to add content. + /// + int Row { get; } + + /// The number of rows visible in the terminal. + int Rows { get; set; } + + /// The topmost row in the terminal. + int Top { get; set; } + + /// Gets whether the supports TrueColor output. + bool SupportsTrueColor { get; } + + /// + /// Gets or sets whether the should use 16 colors instead of the default TrueColors. + /// See to change this setting via . + /// + /// + /// + /// Will be forced to if is + /// , indicating that the cannot support TrueColor. + /// + /// + bool Force16Colors { get; set; } + + /// + /// The that will be used for the next or + /// call. + /// + Attribute CurrentAttribute { get; set; } + + /// Returns the name of the driver and relevant library version information. + /// + string GetVersionInfo (); + + /// + /// Provide proper writing to send escape sequence recognized by the . + /// + /// + void WriteRaw (string ansi); + + /// 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. + /// + bool IsRuneSupported (Rune rune); + + /// 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. + /// + bool IsValidLocation (int col, int row); + + /// Tests whether the specified coordinate are valid for drawing the specified Rune. + /// Used to determine if one or two columns are required. + /// The column. + /// The row. + /// + /// if the coordinate is outside the screen bounds or outside of . + /// otherwise. + /// + bool IsValidLocation (Rune rune, int col, int 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. + void Move (int col, int row); + + /// Adds the specified rune to the display at the current cursor position. + /// + /// + /// When the method returns, will be incremented by the number of columns + /// required, even if the new column value is outside of the or screen + /// dimensions defined by . + /// + /// + /// If requires more than one column, and plus the number of columns + /// needed exceeds the or screen dimensions, the default Unicode replacement character (U+FFFD) + /// will be added instead. + /// + /// + /// Rune to add. + void AddRune (Rune rune); + + /// + /// Adds the specified to the display at the current cursor position. This method is a + /// convenience method that calls with the constructor. + /// + /// Character to add. + void AddRune (char c); + + /// Adds the to the display at the cursor position. + /// + /// + /// When the method returns, will be incremented by the number of columns + /// required, unless the new column value is outside of the or screen + /// dimensions defined by . + /// + /// If requires more columns than are available, the output will be clipped. + /// + /// String. + void AddStr (string str); + + /// Fills the specified rectangle with the specified rune, using + /// + /// The value of is honored. Any parts of the rectangle not in the clip will not be drawn. + /// + /// The Screen-relative rectangle. + /// The Rune used to fill the rectangle + void FillRect (Rectangle rect, Rune rune = default); + + /// + /// Fills the specified rectangle with the specified . This method is a convenience method + /// that calls . + /// + /// + /// + void FillRect (Rectangle rect, char c); + + /// Clears the of the driver. + void ClearContents (); + + /// + /// Raised each time is called. For benchmarking. + /// + event EventHandler? ClearedContents; + + /// + /// Sets as dirty for situations where views + /// don't need layout and redrawing, but just refresh the screen. + /// + void SetContentsAsDirty (); + + /// Determines if the terminal cursor should be visible or not and sets it accordingly. + /// upon success + bool EnsureCursorVisibility (); + + /// Gets the terminal cursor visibility. + /// The current + /// upon success + bool GetCursorVisibility (out CursorVisibility visibility); + + /// Called when the terminal size changes. Fires the event. + /// + void OnSizeChanged (SizeChangedEventArgs args); + + /// Updates the screen to reflect all the changes that have been done to the display buffer + void Refresh (); + + /// + /// Raised each time is called. For benchmarking. + /// + event EventHandler>? Refreshed; + + /// Sets the terminal cursor visibility. + /// The wished + /// upon success + bool SetCursorVisibility (CursorVisibility visibility); + + /// The event fired when the terminal is resized. + event EventHandler? SizeChanged; + + /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. + /// This is only implemented in . + void Suspend (); + + /// Sets the position of the terminal cursor to and . + void UpdateCursor (); + + /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. + /// if any updates to the screen were made. + bool UpdateScreen (); + + /// Initializes the driver + /// Returns an instance of using the for the driver. + MainLoop Init (); + + /// Ends the execution of the console driver. + void End (); + + /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. + /// Implementations should call base.SetAttribute(c). + /// C. + Attribute SetAttribute (Attribute c); + + /// Gets the current . + /// The current attribute. + Attribute GetAttribute (); + + /// Makes an . + /// The foreground color. + /// The background color. + /// The attribute for the foreground and background colors. + Attribute MakeColor (in Color foreground, in Color background); + + /// Event fired when a mouse event occurs. + event EventHandler? MouseEvent; + + /// Called when a mouse event occurs. Fires the event. + /// + void OnMouseEvent (MouseEventArgs a); + + /// Event fired when a key is pressed down. This is a precursor to . + event EventHandler? KeyDown; + + /// + /// Called when a key is pressed down. Fires the event. This is a precursor to + /// . + /// + /// + void OnKeyDown (Key a); + + /// Event fired when a key is released. + /// + /// Drivers that do not support key release events will fire this event after processing is + /// complete. + /// + event EventHandler? KeyUp; + + /// Called when a key is released. Fires the event. + /// + /// Drivers that do not support key release events will call this method after processing + /// is complete. + /// + /// + void OnKeyUp (Key a); + + /// Simulates a key press. + /// The key character. + /// The key. + /// If simulates the Shift key being pressed. + /// If simulates the Alt key being pressed. + /// If simulates the Ctrl key being pressed. + void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); +} diff --git a/Terminal.Gui/ConsoleDrivers/KeyCode.cs b/Terminal.Gui/ConsoleDrivers/KeyCode.cs new file mode 100644 index 0000000000..2a89667ea9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/KeyCode.cs @@ -0,0 +1,321 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// The enumeration encodes key information from s and provides a +/// consistent way for application code to specify keys and receive key events. +/// +/// The class provides a higher-level abstraction, with helper methods and properties for +/// common operations. For example, and provide a convenient way +/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed. +/// +/// +/// +/// +/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a +/// keyboard. Enum values are provided for these (e.g. , , etc.). +/// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent +/// *lowercase*, un-shifted characters. +/// +/// +/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. , +/// , etc.). +/// +/// +/// The shift modifiers (, , and +/// ) can be combined (with logical or) with the other key codes to represent shifted +/// keys. For example, the enum value represents the un-shifted 'a' key, while +/// | represents the 'A' key (shifted 'a' key). Likewise, +/// | represents the 'Alt+A' key combination. +/// +/// +/// All other keys that produce a printable character are encoded as the Unicode value of the character. For +/// example, the for the '!' character is 33, which is the Unicode value for '!'. Likewise, +/// `â` is 226, `Â` is 194, etc. +/// +/// +/// If the is set, then the value is that of the special mask, otherwise, the value is +/// the one of the lower bits (as extracted by ). +/// +/// +[Flags] +public enum KeyCode : uint +{ + /// + /// Mask that indicates that the key is a unicode codepoint. Values outside this range indicate the key has shift + /// modifiers or is a special key like function keys, arrows keys and so on. + /// + CharMask = 0x_f_ffff, + + /// + /// If the is set, then the value is that of the special mask, otherwise, the value is + /// in the lower bits (as extracted by ). + /// + SpecialMask = 0x_fff0_0000, + + /// + /// When this value is set, the Key encodes the sequence Shift-KeyValue. The actual value must be extracted by + /// removing the ShiftMask. + /// + ShiftMask = 0x_1000_0000, + + /// + /// When this value is set, the Key encodes the sequence Alt-KeyValue. The actual value must be extracted by + /// removing the AltMask. + /// + AltMask = 0x_8000_0000, + + /// + /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. The actual value must be extracted by + /// removing the CtrlMask. + /// + CtrlMask = 0x_4000_0000, + + /// The key code representing an invalid or empty key. + Null = 0, + + /// Backspace key. + Backspace = 8, + + /// The key code for the tab key (forwards tab key). + Tab = 9, + + /// The key code for the return key. + Enter = ConsoleKey.Enter, + + /// The key code for the clear key. + Clear = 12, + + /// The key code for the escape key. + Esc = 27, + + /// The key code for the space bar key. + Space = 32, + + /// Digit 0. + D0 = 48, + + /// Digit 1. + D1, + + /// Digit 2. + D2, + + /// Digit 3. + D3, + + /// Digit 4. + D4, + + /// Digit 5. + D5, + + /// Digit 6. + D6, + + /// Digit 7. + D7, + + /// Digit 8. + D8, + + /// Digit 9. + D9, + + /// The key code for the A key + A = 65, + + /// The key code for the B key + B, + + /// The key code for the C key + C, + + /// The key code for the D key + D, + + /// The key code for the E key + E, + + /// The key code for the F key + F, + + /// The key code for the G key + G, + + /// The key code for the H key + H, + + /// The key code for the I key + I, + + /// The key code for the J key + J, + + /// The key code for the K key + K, + + /// The key code for the L key + L, + + /// The key code for the M key + M, + + /// The key code for the N key + N, + + /// The key code for the O key + O, + + /// The key code for the P key + P, + + /// The key code for the Q key + Q, + + /// The key code for the R key + R, + + /// The key code for the S key + S, + + /// The key code for the T key + T, + + /// The key code for the U key + U, + + /// The key code for the V key + V, + + /// The key code for the W key + W, + + /// The key code for the X key + X, + + /// The key code for the Y key + Y, + + /// The key code for the Z key + Z, + + ///// + ///// The key code for the Delete key. + ///// + //Delete = 127, + + // --- Special keys --- + // The values below are common non-alphanum keys. Their values are + // based on the .NET ConsoleKey values, which, in-turn are based on the + // VK_ values from the Windows API. + // We add MaxCodePoint to avoid conflicts with the Unicode values. + + /// The maximum Unicode codepoint value. Used to encode the non-alphanumeric control keys. + MaxCodePoint = 0x10FFFF, + + /// Cursor up key + CursorUp = MaxCodePoint + ConsoleKey.UpArrow, + + /// Cursor down key. + CursorDown = MaxCodePoint + ConsoleKey.DownArrow, + + /// Cursor left key. + CursorLeft = MaxCodePoint + ConsoleKey.LeftArrow, + + /// Cursor right key. + CursorRight = MaxCodePoint + ConsoleKey.RightArrow, + + /// Page Up key. + PageUp = MaxCodePoint + ConsoleKey.PageUp, + + /// Page Down key. + PageDown = MaxCodePoint + ConsoleKey.PageDown, + + /// Home key. + Home = MaxCodePoint + ConsoleKey.Home, + + /// End key. + End = MaxCodePoint + ConsoleKey.End, + + /// Insert (INS) key. + Insert = MaxCodePoint + ConsoleKey.Insert, + + /// Delete (DEL) key. + Delete = MaxCodePoint + ConsoleKey.Delete, + + /// Print screen character key. + PrintScreen = MaxCodePoint + ConsoleKey.PrintScreen, + + /// F1 key. + F1 = MaxCodePoint + ConsoleKey.F1, + + /// F2 key. + F2 = MaxCodePoint + ConsoleKey.F2, + + /// F3 key. + F3 = MaxCodePoint + ConsoleKey.F3, + + /// F4 key. + F4 = MaxCodePoint + ConsoleKey.F4, + + /// F5 key. + F5 = MaxCodePoint + ConsoleKey.F5, + + /// F6 key. + F6 = MaxCodePoint + ConsoleKey.F6, + + /// F7 key. + F7 = MaxCodePoint + ConsoleKey.F7, + + /// F8 key. + F8 = MaxCodePoint + ConsoleKey.F8, + + /// F9 key. + F9 = MaxCodePoint + ConsoleKey.F9, + + /// F10 key. + F10 = MaxCodePoint + ConsoleKey.F10, + + /// F11 key. + F11 = MaxCodePoint + ConsoleKey.F11, + + /// F12 key. + F12 = MaxCodePoint + ConsoleKey.F12, + + /// F13 key. + F13 = MaxCodePoint + ConsoleKey.F13, + + /// F14 key. + F14 = MaxCodePoint + ConsoleKey.F14, + + /// F15 key. + F15 = MaxCodePoint + ConsoleKey.F15, + + /// F16 key. + F16 = MaxCodePoint + ConsoleKey.F16, + + /// F17 key. + F17 = MaxCodePoint + ConsoleKey.F17, + + /// F18 key. + F18 = MaxCodePoint + ConsoleKey.F18, + + /// F19 key. + F19 = MaxCodePoint + ConsoleKey.F19, + + /// F20 key. + F20 = MaxCodePoint + ConsoleKey.F20, + + /// F21 key. + F21 = MaxCodePoint + ConsoleKey.F21, + + /// F22 key. + F22 = MaxCodePoint + ConsoleKey.F22, + + /// F23 key. + F23 = MaxCodePoint + ConsoleKey.F23, + + /// F24 key. + F24 = MaxCodePoint + ConsoleKey.F24 +} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs deleted file mode 100644 index a5afbf2580..0000000000 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ /dev/null @@ -1,1827 +0,0 @@ -// -// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient. -// - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; -using static Terminal.Gui.NetEvents; - -namespace Terminal.Gui; - -internal class NetWinVTConsole -{ - private const uint DISABLE_NEWLINE_AUTO_RETURN = 8; - private const uint ENABLE_ECHO_INPUT = 4; - private const uint ENABLE_EXTENDED_FLAGS = 128; - private const uint ENABLE_INSERT_MODE = 32; - private const uint ENABLE_LINE_INPUT = 2; - private const uint ENABLE_LVB_GRID_WORLDWIDE = 10; - private const uint ENABLE_MOUSE_INPUT = 16; - - // Input modes. - private const uint ENABLE_PROCESSED_INPUT = 1; - - // Output modes. - private const uint ENABLE_PROCESSED_OUTPUT = 1; - private const uint ENABLE_QUICK_EDIT_MODE = 64; - private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512; - private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4; - private const uint ENABLE_WINDOW_INPUT = 8; - private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2; - private const int STD_ERROR_HANDLE = -12; - private const int STD_INPUT_HANDLE = -10; - private const int STD_OUTPUT_HANDLE = -11; - - private readonly nint _errorHandle; - private readonly nint _inputHandle; - private readonly uint _originalErrorConsoleMode; - private readonly uint _originalInputConsoleMode; - private readonly uint _originalOutputConsoleMode; - private readonly nint _outputHandle; - - public NetWinVTConsole () - { - _inputHandle = GetStdHandle (STD_INPUT_HANDLE); - - if (!GetConsoleMode (_inputHandle, out uint mode)) - { - throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}."); - } - - _originalInputConsoleMode = mode; - - if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT) - { - mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; - - if (!SetConsoleMode (_inputHandle, mode)) - { - throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}."); - } - } - - _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); - - if (!GetConsoleMode (_outputHandle, out mode)) - { - throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}."); - } - - _originalOutputConsoleMode = mode; - - if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN) - { - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; - - if (!SetConsoleMode (_outputHandle, mode)) - { - throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}."); - } - } - - _errorHandle = GetStdHandle (STD_ERROR_HANDLE); - - if (!GetConsoleMode (_errorHandle, out mode)) - { - throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}."); - } - - _originalErrorConsoleMode = mode; - - if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN) - { - mode |= DISABLE_NEWLINE_AUTO_RETURN; - - if (!SetConsoleMode (_errorHandle, mode)) - { - throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}."); - } - } - } - - public void Cleanup () - { - if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode)) - { - throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}."); - } - - if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode)) - { - throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}."); - } - - if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode)) - { - throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}."); - } - } - - [DllImport ("kernel32.dll")] - private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); - - [DllImport ("kernel32.dll")] - private static extern uint GetLastError (); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint GetStdHandle (int nStdHandle); - - [DllImport ("kernel32.dll")] - private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); -} - -internal class NetEvents : IDisposable -{ - private readonly ManualResetEventSlim _inputReady = new (false); - private CancellationTokenSource _inputReadyCancellationTokenSource; - private readonly ManualResetEventSlim _waitForStart = new (false); - - //CancellationTokenSource _waitForStartCancellationTokenSource; - private readonly ManualResetEventSlim _winChange = new (false); - private readonly Queue _inputQueue = new (); - private readonly ConsoleDriver _consoleDriver; - private ConsoleKeyInfo [] _cki; - private bool _isEscSeq; -#if PROCESS_REQUEST - bool _neededProcessRequest; -#endif - public EscSeqRequests EscSeqRequests { get; } = new (); - - public NetEvents (ConsoleDriver consoleDriver) - { - _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); - _inputReadyCancellationTokenSource = new CancellationTokenSource (); - - Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); - - Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); - } - - public InputResult? DequeueInput () - { - while (_inputReadyCancellationTokenSource != null - && !_inputReadyCancellationTokenSource.Token.IsCancellationRequested) - { - _waitForStart.Set (); - _winChange.Set (); - - try - { - if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) - { - if (_inputQueue.Count == 0) - { - _inputReady.Wait (_inputReadyCancellationTokenSource.Token); - } - } - } - catch (OperationCanceledException) - { - return null; - } - finally - { - _inputReady.Reset (); - } - -#if PROCESS_REQUEST - _neededProcessRequest = false; -#endif - if (_inputQueue.Count > 0) - { - return _inputQueue.Dequeue (); - } - } - - return null; - } - - private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) - { - // if there is a key available, return it without waiting - // (or dispatching work to the thread queue) - if (Console.KeyAvailable) - { - return Console.ReadKey (intercept); - } - - while (!cancellationToken.IsCancellationRequested) - { - Task.Delay (100, cancellationToken).Wait (cancellationToken); - - if (Console.KeyAvailable) - { - return Console.ReadKey (intercept); - } - } - - cancellationToken.ThrowIfCancellationRequested (); - - return default (ConsoleKeyInfo); - } - - private void ProcessInputQueue () - { - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) - { - try - { - _waitForStart.Wait (_inputReadyCancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - return; - } - - _waitForStart.Reset (); - - if (_inputQueue.Count == 0) - { - ConsoleKey key = 0; - ConsoleModifiers mod = 0; - ConsoleKeyInfo newConsoleKeyInfo = default; - - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) - { - ConsoleKeyInfo consoleKeyInfo; - - try - { - consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - return; - } - - if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) - || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) - { - if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) - { - _cki = EscSeqUtils.ResizeArray ( - new ConsoleKeyInfo ( - (char)KeyCode.Esc, - 0, - false, - false, - false - ), - _cki - ); - } - - _isEscSeq = true; - newConsoleKeyInfo = consoleKeyInfo; - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - - if (Console.KeyAvailable) - { - continue; - } - - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - _isEscSeq = false; - - break; - } - - if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { }) - { - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - - if (Console.KeyAvailable) - { - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - } - else - { - ProcessMapConsoleKeyInfo (consoleKeyInfo); - } - - break; - } - - ProcessMapConsoleKeyInfo (consoleKeyInfo); - - break; - } - } - - _inputReady.Set (); - } - - void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) - { - _inputQueue.Enqueue ( - new InputResult - { - EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) - } - ); - _isEscSeq = false; - } - } - - private void CheckWindowSizeChange () - { - void RequestWindowSize (CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - // Wait for a while then check if screen has changed sizes - Task.Delay (500, cancellationToken).Wait (cancellationToken); - - int buffHeight, buffWidth; - - if (((NetDriver)_consoleDriver).IsWinPlatform) - { - buffHeight = Math.Max (Console.BufferHeight, 0); - buffWidth = Math.Max (Console.BufferWidth, 0); - } - else - { - buffHeight = _consoleDriver.Rows; - buffWidth = _consoleDriver.Cols; - } - - if (EnqueueWindowSizeEvent ( - Math.Max (Console.WindowHeight, 0), - Math.Max (Console.WindowWidth, 0), - buffHeight, - buffWidth - )) - { - return; - } - } - - cancellationToken.ThrowIfCancellationRequested (); - } - - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) - { - try - { - _winChange.Wait (_inputReadyCancellationTokenSource.Token); - _winChange.Reset (); - - RequestWindowSize (_inputReadyCancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - return; - } - - _inputReady.Set (); - } - } - - /// Enqueue a window size event if the window size has changed. - /// - /// - /// - /// - /// - private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth) - { - if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows) - { - return false; - } - - int w = Math.Max (winWidth, 0); - int h = Math.Max (winHeight, 0); - - _inputQueue.Enqueue ( - new InputResult - { - EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) } - } - ); - - return true; - } - - // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) - private void ProcessRequestResponse ( - ref ConsoleKeyInfo newConsoleKeyInfo, - ref ConsoleKey key, - ConsoleKeyInfo [] cki, - ref ConsoleModifiers mod - ) - { - // isMouse is true if it's CSI<, false otherwise - EscSeqUtils.DecodeEscSeq ( - EscSeqRequests, - ref newConsoleKeyInfo, - ref key, - cki, - ref mod, - out string c1Control, - out string code, - out string [] values, - out string terminating, - out bool isMouse, - out List mouseFlags, - out Point pos, - out bool isReq, - (f, p) => HandleMouseEvent (MapMouseFlags (f), p) - ); - - if (isMouse) - { - foreach (MouseFlags mf in mouseFlags) - { - HandleMouseEvent (MapMouseFlags (mf), pos); - } - - return; - } - - if (isReq) - { - HandleRequestResponseEvent (c1Control, code, values, terminating); - - return; - } - - HandleKeyboardEvent (newConsoleKeyInfo); - } - - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] - private MouseButtonState MapMouseFlags (MouseFlags mouseFlags) - { - MouseButtonState mbs = default; - - foreach (object flag in Enum.GetValues (mouseFlags.GetType ())) - { - if (mouseFlags.HasFlag ((MouseFlags)flag)) - { - switch (flag) - { - case MouseFlags.Button1Pressed: - mbs |= MouseButtonState.Button1Pressed; - - break; - case MouseFlags.Button1Released: - mbs |= MouseButtonState.Button1Released; - - break; - case MouseFlags.Button1Clicked: - mbs |= MouseButtonState.Button1Clicked; - - break; - case MouseFlags.Button1DoubleClicked: - mbs |= MouseButtonState.Button1DoubleClicked; - - break; - case MouseFlags.Button1TripleClicked: - mbs |= MouseButtonState.Button1TripleClicked; - - break; - case MouseFlags.Button2Pressed: - mbs |= MouseButtonState.Button2Pressed; - - break; - case MouseFlags.Button2Released: - mbs |= MouseButtonState.Button2Released; - - break; - case MouseFlags.Button2Clicked: - mbs |= MouseButtonState.Button2Clicked; - - break; - case MouseFlags.Button2DoubleClicked: - mbs |= MouseButtonState.Button2DoubleClicked; - - break; - case MouseFlags.Button2TripleClicked: - mbs |= MouseButtonState.Button2TripleClicked; - - break; - case MouseFlags.Button3Pressed: - mbs |= MouseButtonState.Button3Pressed; - - break; - case MouseFlags.Button3Released: - mbs |= MouseButtonState.Button3Released; - - break; - case MouseFlags.Button3Clicked: - mbs |= MouseButtonState.Button3Clicked; - - break; - case MouseFlags.Button3DoubleClicked: - mbs |= MouseButtonState.Button3DoubleClicked; - - break; - case MouseFlags.Button3TripleClicked: - mbs |= MouseButtonState.Button3TripleClicked; - - break; - case MouseFlags.WheeledUp: - mbs |= MouseButtonState.ButtonWheeledUp; - - break; - case MouseFlags.WheeledDown: - mbs |= MouseButtonState.ButtonWheeledDown; - - break; - case MouseFlags.WheeledLeft: - mbs |= MouseButtonState.ButtonWheeledLeft; - - break; - case MouseFlags.WheeledRight: - mbs |= MouseButtonState.ButtonWheeledRight; - - break; - case MouseFlags.Button4Pressed: - mbs |= MouseButtonState.Button4Pressed; - - break; - case MouseFlags.Button4Released: - mbs |= MouseButtonState.Button4Released; - - break; - case MouseFlags.Button4Clicked: - mbs |= MouseButtonState.Button4Clicked; - - break; - case MouseFlags.Button4DoubleClicked: - mbs |= MouseButtonState.Button4DoubleClicked; - - break; - case MouseFlags.Button4TripleClicked: - mbs |= MouseButtonState.Button4TripleClicked; - - break; - case MouseFlags.ButtonShift: - mbs |= MouseButtonState.ButtonShift; - - break; - case MouseFlags.ButtonCtrl: - mbs |= MouseButtonState.ButtonCtrl; - - break; - case MouseFlags.ButtonAlt: - mbs |= MouseButtonState.ButtonAlt; - - break; - case MouseFlags.ReportMousePosition: - mbs |= MouseButtonState.ReportMousePosition; - - break; - case MouseFlags.AllEvents: - mbs |= MouseButtonState.AllEvents; - - break; - } - } - } - - return mbs; - } - - private Point _lastCursorPosition; - - private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating) - { - switch (terminating) - { - // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed. - case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator: - var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 }; - - if (_lastCursorPosition.Y != point.Y) - { - _lastCursorPosition = point; - var eventType = EventType.WindowPosition; - var winPositionEv = new WindowPositionEvent { CursorPosition = point }; - - _inputQueue.Enqueue ( - new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv } - ); - } - else - { - return; - } - - break; - - case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator: - switch (values [0]) - { - case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue: - EnqueueWindowSizeEvent ( - Math.Max (int.Parse (values [1]), 0), - Math.Max (int.Parse (values [2]), 0), - Math.Max (int.Parse (values [1]), 0), - Math.Max (int.Parse (values [2]), 0) - ); - - break; - default: - EnqueueRequestResponseEvent (c1Control, code, values, terminating); - - break; - } - - break; - default: - EnqueueRequestResponseEvent (c1Control, code, values, terminating); - - break; - } - - _inputReady.Set (); - } - - private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating) - { - var eventType = EventType.RequestResponse; - var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) }; - - _inputQueue.Enqueue ( - new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv } - ); - } - - private void HandleMouseEvent (MouseButtonState buttonState, Point pos) - { - var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState }; - - _inputQueue.Enqueue ( - new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent } - ); - - _inputReady.Set (); - } - - public enum EventType - { - Key = 1, - Mouse = 2, - WindowSize = 3, - WindowPosition = 4, - RequestResponse = 5 - } - - [Flags] - public enum MouseButtonState - { - Button1Pressed = 0x1, - Button1Released = 0x2, - Button1Clicked = 0x4, - Button1DoubleClicked = 0x8, - Button1TripleClicked = 0x10, - Button2Pressed = 0x20, - Button2Released = 0x40, - Button2Clicked = 0x80, - Button2DoubleClicked = 0x100, - Button2TripleClicked = 0x200, - Button3Pressed = 0x400, - Button3Released = 0x800, - Button3Clicked = 0x1000, - Button3DoubleClicked = 0x2000, - Button3TripleClicked = 0x4000, - ButtonWheeledUp = 0x8000, - ButtonWheeledDown = 0x10000, - ButtonWheeledLeft = 0x20000, - ButtonWheeledRight = 0x40000, - Button4Pressed = 0x80000, - Button4Released = 0x100000, - Button4Clicked = 0x200000, - Button4DoubleClicked = 0x400000, - Button4TripleClicked = 0x800000, - ButtonShift = 0x1000000, - ButtonCtrl = 0x2000000, - ButtonAlt = 0x4000000, - ReportMousePosition = 0x8000000, - AllEvents = -1 - } - - public struct MouseEvent - { - public Point Position; - public MouseButtonState ButtonState; - } - - public struct WindowSizeEvent - { - public Size Size; - } - - public struct WindowPositionEvent - { - public int Top; - public int Left; - public Point CursorPosition; - } - - public struct RequestResponseEvent - { - public (string c1Control, string code, string [] values, string terminating) ResultTuple; - } - - public struct InputResult - { - public EventType EventType; - public ConsoleKeyInfo ConsoleKeyInfo; - public MouseEvent MouseEvent; - public WindowSizeEvent WindowSizeEvent; - public WindowPositionEvent WindowPositionEvent; - public RequestResponseEvent RequestResponseEvent; - - public readonly override string ToString () - { - return EventType switch - { - EventType.Key => ToString (ConsoleKeyInfo), - EventType.Mouse => MouseEvent.ToString (), - - //EventType.WindowSize => WindowSize.ToString (), - //EventType.RequestResponse => RequestResponse.ToString (), - _ => "Unknown event type: " + EventType - }; - } - - /// Prints a ConsoleKeyInfoEx structure - /// - /// - public readonly string ToString (ConsoleKeyInfo cki) - { - var ke = new Key ((KeyCode)cki.KeyChar); - var sb = new StringBuilder (); - sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})"); - sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); - sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); - sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); - sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) "); - string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); - - return $"[ConsoleKeyInfo({s})]"; - } - } - - private void HandleKeyboardEvent (ConsoleKeyInfo cki) - { - var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki }; - - _inputQueue.Enqueue (inputResult); - } - - public void Dispose () - { - _inputReadyCancellationTokenSource?.Cancel (); - _inputReadyCancellationTokenSource?.Dispose (); - _inputReadyCancellationTokenSource = null; - - try - { - // throws away any typeahead that has been typed by - // the user and has not yet been read by the program. - while (Console.KeyAvailable) - { - Console.ReadKey (true); - } - } - catch (InvalidOperationException) - { - // Ignore - Console input has already been closed - } - } -} - -internal class NetDriver : ConsoleDriver -{ - private const int COLOR_BLACK = 30; - private const int COLOR_BLUE = 34; - private const int COLOR_BRIGHT_BLACK = 90; - private const int COLOR_BRIGHT_BLUE = 94; - private const int COLOR_BRIGHT_CYAN = 96; - private const int COLOR_BRIGHT_GREEN = 92; - private const int COLOR_BRIGHT_MAGENTA = 95; - private const int COLOR_BRIGHT_RED = 91; - private const int COLOR_BRIGHT_WHITE = 97; - private const int COLOR_BRIGHT_YELLOW = 93; - private const int COLOR_CYAN = 36; - private const int COLOR_GREEN = 32; - private const int COLOR_MAGENTA = 35; - private const int COLOR_RED = 31; - private const int COLOR_WHITE = 37; - private const int COLOR_YELLOW = 33; - private NetMainLoop _mainLoopDriver; - public bool IsWinPlatform { get; private set; } - public NetWinVTConsole NetWinConsole { get; private set; } - - public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix - || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931); - - public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) - { - var input = new InputResult - { - EventType = EventType.Key, ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control) - }; - - try - { - ProcessInput (input); - } - catch (OverflowException) - { } - } - - public override void Suspend () - { - if (Environment.OSVersion.Platform != PlatformID.Unix) - { - return; - } - - StopReportingMouseMoves (); - - if (!RunningUnitTests) - { - Console.ResetColor (); - Console.Clear (); - - //Disable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); - - //Set cursor key to cursor. - Console.Out.Write (EscSeqUtils.CSI_ShowCursor); - - Platform.Suspend (); - - //Enable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - - SetContentsAsDirty (); - Refresh (); - } - - StartReportingMouseMoves (); - } - - public override bool UpdateScreen () - { - bool updated = false; - if (RunningUnitTests - || _winSizeChanging - || Console.WindowHeight < 1 - || Contents.Length != Rows * Cols - || Rows != Console.WindowHeight) - { - return updated; - } - - var top = 0; - var left = 0; - int rows = Rows; - int cols = Cols; - var output = new StringBuilder (); - Attribute? redrawAttr = null; - int lastCol = -1; - - CursorVisibility? savedVisibility = _cachedCursorVisibility; - SetCursorVisibility (CursorVisibility.Invisible); - - for (int row = top; row < rows; row++) - { - if (Console.WindowHeight < 1) - { - return updated; - } - - if (!_dirtyLines [row]) - { - continue; - } - - if (!SetCursorPosition (0, row)) - { - return updated; - } - - updated = true; - _dirtyLines [row] = false; - output.Clear (); - - for (int col = left; col < cols; col++) - { - lastCol = -1; - var outputWidth = 0; - - for (; col < cols; col++) - { - if (!Contents [row, col].IsDirty) - { - if (output.Length > 0) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (lastCol == -1) - { - lastCol = col; - } - - if (lastCol + 1 < cols) - { - lastCol++; - } - - continue; - } - - if (lastCol == -1) - { - lastCol = col; - } - - Attribute attr = Contents [row, col].Attribute.Value; - - // Performance: Only send the escape sequence if the attribute has changed. - if (attr != redrawAttr) - { - redrawAttr = attr; - - if (Force16Colors) - { - output.Append ( - EscSeqUtils.CSI_SetGraphicsRendition ( - MapColors ( - (ConsoleColor)attr.Background.GetClosestNamedColor16 (), - false - ), - MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ()) - ) - ); - } - else - { - output.Append ( - EscSeqUtils.CSI_SetForegroundColorRGB ( - attr.Foreground.R, - attr.Foreground.G, - attr.Foreground.B - ) - ); - - output.Append ( - EscSeqUtils.CSI_SetBackgroundColorRGB ( - attr.Background.R, - attr.Background.G, - attr.Background.B - ) - ); - } - } - - outputWidth++; - Rune rune = Contents [row, col].Rune; - output.Append (rune); - - if (Contents [row, col].CombiningMarks.Count > 0) - { - // AtlasEngine does not support NON-NORMALIZED combining marks in a way - // compatible with the driver architecture. Any CMs (except in the first col) - // are correctly combined with the base char, but are ALSO treated as 1 column - // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. - // - // For now, we just ignore the list of CMs. - //foreach (var combMark in Contents [row, col].CombiningMarks) { - // output.Append (combMark); - //} - // WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPosition (col - 1, row); - } - - Contents [row, col].IsDirty = false; - } - } - - if (output.Length > 0) - { - SetCursorPosition (lastCol, row); - Console.Write (output); - } - - foreach (var s in Application.Sixel) - { - if (!string.IsNullOrWhiteSpace (s.SixelData)) - { - SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Write (s.SixelData); - } - } - } - - SetCursorPosition (0, 0); - - _cachedCursorVisibility = savedVisibility; - - void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) - { - SetCursorPosition (lastCol, row); - Console.Write (output); - output.Clear (); - lastCol += outputWidth; - outputWidth = 0; - } - - return updated; - } - - internal override void End () - { - if (IsWinPlatform) - { - NetWinConsole?.Cleanup (); - } - - StopReportingMouseMoves (); - - if (!RunningUnitTests) - { - Console.ResetColor (); - - //Disable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); - - //Set cursor key to cursor. - Console.Out.Write (EscSeqUtils.CSI_ShowCursor); - Console.Out.Close (); - } - } - - internal override MainLoop Init () - { - PlatformID p = Environment.OSVersion.Platform; - - if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) - { - IsWinPlatform = true; - - try - { - NetWinConsole = new NetWinVTConsole (); - } - catch (ApplicationException) - { - // Likely running as a unit test, or in a non-interactive session. - } - } - - if (IsWinPlatform) - { - Clipboard = new WindowsClipboard (); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) - { - Clipboard = new MacOSXClipboard (); - } - else - { - if (CursesDriver.Is_WSL_Platform ()) - { - Clipboard = new WSLClipboard (); - } - else - { - Clipboard = new CursesClipboard (); - } - } - - if (!RunningUnitTests) - { - Console.TreatControlCAsInput = true; - - Cols = Console.WindowWidth; - Rows = Console.WindowHeight; - - //Enable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - - //Set cursor key to application. - Console.Out.Write (EscSeqUtils.CSI_HideCursor); - } - else - { - // We are being run in an environment that does not support a console - // such as a unit test, or a pipe. - Cols = 80; - Rows = 24; - } - - ResizeScreen (); - ClearContents (); - CurrentAttribute = new Attribute (Color.White, Color.Black); - - StartReportingMouseMoves (); - - _mainLoopDriver = new NetMainLoop (this); - _mainLoopDriver.ProcessInput = ProcessInput; - - - return new MainLoop (_mainLoopDriver); - } - - private void ProcessInput (InputResult inputEvent) - { - switch (inputEvent.EventType) - { - case EventType.Key: - ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo; - - //if (consoleKeyInfo.Key == ConsoleKey.Packet) { - // consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo); - //} - - //Debug.WriteLine ($"event: {inputEvent}"); - - KeyCode map = MapKey (consoleKeyInfo); - - if (map == KeyCode.Null) - { - break; - } - - OnKeyDown (new Key (map)); - OnKeyUp (new Key (map)); - - break; - case EventType.Mouse: - MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); - //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}"); - OnMouseEvent (me); - - break; - case EventType.WindowSize: - _winSizeChanging = true; - Top = 0; - Left = 0; - Cols = inputEvent.WindowSizeEvent.Size.Width; - Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0); - ; - ResizeScreen (); - ClearContents (); - _winSizeChanging = false; - OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); - - break; - case EventType.RequestResponse: - break; - case EventType.WindowPosition: - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - - #region Size and Position Handling - - private volatile bool _winSizeChanging; - - private void SetWindowPosition (int col, int row) - { - if (!RunningUnitTests) - { - Top = Console.WindowTop; - Left = Console.WindowLeft; - } - else - { - Top = row; - Left = col; - } - } - - public virtual void ResizeScreen () - { - // Not supported on Unix. - if (IsWinPlatform) - { - // Can raise an exception while is still resizing. - try - { -#pragma warning disable CA1416 - if (Console.WindowHeight > 0) - { - Console.CursorTop = 0; - Console.CursorLeft = 0; - Console.WindowTop = 0; - Console.WindowLeft = 0; - - if (Console.WindowHeight > Rows) - { - Console.SetWindowSize (Cols, Rows); - } - - Console.SetBufferSize (Cols, Rows); - } -#pragma warning restore CA1416 - } - // INTENT: Why are these eating the exceptions? - // Comments would be good here. - catch (IOException) - { - // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (Screen); - } - catch (ArgumentOutOfRangeException) - { - // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (Screen); - } - } - else - { - Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols)); - } - - // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (Screen); - } - - #endregion - - #region Color Handling - - // Cache the list of ConsoleColor values. - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] - private static readonly HashSet ConsoleColorValues = new ( - Enum.GetValues (typeof (ConsoleColor)) - .OfType () - .Select (c => (int)c) - ); - - // Dictionary for mapping ConsoleColor values to the values used by System.Net.Console. - private static readonly Dictionary colorMap = new () - { - { ConsoleColor.Black, COLOR_BLACK }, - { ConsoleColor.DarkBlue, COLOR_BLUE }, - { ConsoleColor.DarkGreen, COLOR_GREEN }, - { ConsoleColor.DarkCyan, COLOR_CYAN }, - { ConsoleColor.DarkRed, COLOR_RED }, - { ConsoleColor.DarkMagenta, COLOR_MAGENTA }, - { ConsoleColor.DarkYellow, COLOR_YELLOW }, - { ConsoleColor.Gray, COLOR_WHITE }, - { ConsoleColor.DarkGray, COLOR_BRIGHT_BLACK }, - { ConsoleColor.Blue, COLOR_BRIGHT_BLUE }, - { ConsoleColor.Green, COLOR_BRIGHT_GREEN }, - { ConsoleColor.Cyan, COLOR_BRIGHT_CYAN }, - { ConsoleColor.Red, COLOR_BRIGHT_RED }, - { ConsoleColor.Magenta, COLOR_BRIGHT_MAGENTA }, - { ConsoleColor.Yellow, COLOR_BRIGHT_YELLOW }, - { ConsoleColor.White, COLOR_BRIGHT_WHITE } - }; - - // Map a ConsoleColor to a platform dependent value. - private int MapColors (ConsoleColor color, bool isForeground = true) - { - return colorMap.TryGetValue (color, out int colorValue) ? colorValue + (isForeground ? 0 : 10) : 0; - } - - ///// - ///// In the NetDriver, colors are encoded as an int. - ///// However, the foreground color is stored in the most significant 16 bits, - ///// and the background color is stored in the least significant 16 bits. - ///// - //public override Attribute MakeColor (Color foreground, Color background) - //{ - // // Encode the colors into the int value. - // return new Attribute ( - // platformColor: ((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff), - // foreground: foreground, - // background: background - // ); - //} - - #endregion - - #region Cursor Handling - - private bool SetCursorPosition (int col, int row) - { - if (IsWinPlatform) - { - // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth. - try - { - Console.SetCursorPosition (col, row); - - return true; - } - catch (Exception) - { - return false; - } - } - - // + 1 is needed because non-Windows is based on 1 instead of 0 and - // Console.CursorTop/CursorLeft isn't reliable. - Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1)); - - return true; - } - - private CursorVisibility? _cachedCursorVisibility; - - public override void UpdateCursor () - { - EnsureCursorVisibility (); - - if (Col >= 0 && Col < Cols && Row >= 0 && Row <= Rows) - { - SetCursorPosition (Col, Row); - SetWindowPosition (0, Row); - } - } - - public override bool GetCursorVisibility (out CursorVisibility visibility) - { - visibility = _cachedCursorVisibility ?? CursorVisibility.Default; - - return visibility == CursorVisibility.Default; - } - - public override bool SetCursorVisibility (CursorVisibility visibility) - { - _cachedCursorVisibility = visibility; - - Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - - return visibility == CursorVisibility.Default; - } - - public override bool EnsureCursorVisibility () - { - if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) - { - GetCursorVisibility (out CursorVisibility cursorVisibility); - _cachedCursorVisibility = cursorVisibility; - SetCursorVisibility (CursorVisibility.Invisible); - - return false; - } - - SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default); - - return _cachedCursorVisibility == CursorVisibility.Default; - } - - #endregion - - #region Mouse Handling - - public void StartReportingMouseMoves () - { - if (!RunningUnitTests) - { - Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); - } - } - - public void StopReportingMouseMoves () - { - if (!RunningUnitTests) - { - Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); - } - } - - private MouseEventArgs ToDriverMouse (NetEvents.MouseEvent me) - { - //System.Diagnostics.Debug.WriteLine ($"X: {me.Position.X}; Y: {me.Position.Y}; ButtonState: {me.ButtonState}"); - - MouseFlags mouseFlag = 0; - - if ((me.ButtonState & MouseButtonState.Button1Pressed) != 0) - { - mouseFlag |= MouseFlags.Button1Pressed; - } - - if ((me.ButtonState & MouseButtonState.Button1Released) != 0) - { - mouseFlag |= MouseFlags.Button1Released; - } - - if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0) - { - mouseFlag |= MouseFlags.Button1Clicked; - } - - if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0) - { - mouseFlag |= MouseFlags.Button1DoubleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0) - { - mouseFlag |= MouseFlags.Button1TripleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0) - { - mouseFlag |= MouseFlags.Button2Pressed; - } - - if ((me.ButtonState & MouseButtonState.Button2Released) != 0) - { - mouseFlag |= MouseFlags.Button2Released; - } - - if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0) - { - mouseFlag |= MouseFlags.Button2Clicked; - } - - if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0) - { - mouseFlag |= MouseFlags.Button2DoubleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0) - { - mouseFlag |= MouseFlags.Button2TripleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0) - { - mouseFlag |= MouseFlags.Button3Pressed; - } - - if ((me.ButtonState & MouseButtonState.Button3Released) != 0) - { - mouseFlag |= MouseFlags.Button3Released; - } - - if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0) - { - mouseFlag |= MouseFlags.Button3Clicked; - } - - if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0) - { - mouseFlag |= MouseFlags.Button3DoubleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0) - { - mouseFlag |= MouseFlags.Button3TripleClicked; - } - - if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0) - { - mouseFlag |= MouseFlags.WheeledUp; - } - - if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0) - { - mouseFlag |= MouseFlags.WheeledDown; - } - - if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0) - { - mouseFlag |= MouseFlags.WheeledLeft; - } - - if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0) - { - mouseFlag |= MouseFlags.WheeledRight; - } - - if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0) - { - mouseFlag |= MouseFlags.Button4Pressed; - } - - if ((me.ButtonState & MouseButtonState.Button4Released) != 0) - { - mouseFlag |= MouseFlags.Button4Released; - } - - if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0) - { - mouseFlag |= MouseFlags.Button4Clicked; - } - - if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0) - { - mouseFlag |= MouseFlags.Button4DoubleClicked; - } - - if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0) - { - mouseFlag |= MouseFlags.Button4TripleClicked; - } - - if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0) - { - mouseFlag |= MouseFlags.ReportMousePosition; - } - - if ((me.ButtonState & MouseButtonState.ButtonShift) != 0) - { - mouseFlag |= MouseFlags.ButtonShift; - } - - if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0) - { - mouseFlag |= MouseFlags.ButtonCtrl; - } - - if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0) - { - mouseFlag |= MouseFlags.ButtonAlt; - } - - return new MouseEventArgs { Position = me.Position, Flags = mouseFlag }; - } - - #endregion Mouse Handling - - #region Keyboard Handling - - private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) - { - if (consoleKeyInfo.Key != ConsoleKey.Packet) - { - return consoleKeyInfo; - } - - ConsoleModifiers mod = consoleKeyInfo.Modifiers; - bool shift = (mod & ConsoleModifiers.Shift) != 0; - bool alt = (mod & ConsoleModifiers.Alt) != 0; - bool control = (mod & ConsoleModifiers.Control) != 0; - - ConsoleKeyInfo cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo); - - return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control); - } - - private KeyCode MapKey (ConsoleKeyInfo keyInfo) - { - switch (keyInfo.Key) - { - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - case ConsoleKey.Packet: - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - if (keyInfo.KeyChar == 0) - { - // If the keyChar is 0, keyInfo.Key value is not a printable character. - - return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers != ConsoleModifiers.Shift) - { - // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") - // and passing on Shift would be redundant. - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); - } - - // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC - if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) - { - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I) - { - return KeyCode.Tab; - } - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key)); - } - - // Handle control keys (e.g. CursorUp) - if (keyInfo.Key != ConsoleKey.None - && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); - } - - if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Shifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)keyInfo.KeyChar, - true, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Unshifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)(keyInfo.KeyChar - 32), - false, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z ) - { - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) - || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers == ConsoleModifiers.Shift) - { - // If ShiftMask is on add the ShiftMask - if (char.IsUpper (keyInfo.KeyChar)) - { - return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; - } - } - - return (KeyCode)keyInfo.Key; - } - - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar)); - } - - #endregion Keyboard Handling -} - -/// -/// Mainloop intended to be used with the .NET System.Console API, and can be used on Windows and Unix, it is -/// cross-platform but lacks things like file descriptor monitoring. -/// -/// This implementation is used for NetDriver. -internal class NetMainLoop : IMainLoopDriver -{ - internal NetEvents _netEvents; - - /// Invoked when a Key is pressed. - internal Action ProcessInput; - - private readonly ManualResetEventSlim _eventReady = new (false); - private readonly CancellationTokenSource _inputHandlerTokenSource = new (); - private readonly Queue _resultQueue = new (); - private readonly ManualResetEventSlim _waitForProbe = new (false); - private readonly CancellationTokenSource _eventReadyTokenSource = new (); - private MainLoop _mainLoop; - - /// Initializes the class with the console driver. - /// Passing a consoleDriver is provided to capture windows resizing. - /// The console driver used by this Net main loop. - /// - public NetMainLoop (ConsoleDriver consoleDriver = null) - { - if (consoleDriver is null) - { - throw new ArgumentNullException (nameof (consoleDriver)); - } - - _netEvents = new NetEvents (consoleDriver); - } - - void IMainLoopDriver.Setup (MainLoop mainLoop) - { - _mainLoop = mainLoop; - Task.Run (NetInputHandler, _inputHandlerTokenSource.Token); - } - - void IMainLoopDriver.Wakeup () { _eventReady.Set (); } - - bool IMainLoopDriver.EventsPending () - { - _waitForProbe.Set (); - - if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout)) - { - return true; - } - - try - { - if (!_eventReadyTokenSource.IsCancellationRequested) - { - // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there - // are no timers, but there IS an idle handler waiting. - _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); - } - } - catch (OperationCanceledException) - { - return true; - } - finally - { - _eventReady.Reset (); - } - - _eventReadyTokenSource.Token.ThrowIfCancellationRequested (); - - if (!_eventReadyTokenSource.IsCancellationRequested) - { - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); - } - - return true; - } - - void IMainLoopDriver.Iteration () - { - while (_resultQueue.Count > 0) - { - ProcessInput?.Invoke (_resultQueue.Dequeue ().Value); - } - } - - void IMainLoopDriver.TearDown () - { - _inputHandlerTokenSource?.Cancel (); - _inputHandlerTokenSource?.Dispose (); - _eventReadyTokenSource?.Cancel (); - _eventReadyTokenSource?.Dispose (); - - _eventReady?.Dispose (); - - _resultQueue?.Clear (); - _waitForProbe?.Dispose (); - _netEvents?.Dispose (); - _netEvents = null; - - _mainLoop = null; - } - - private void NetInputHandler () - { - while (_mainLoop is { }) - { - try - { - if (!_inputHandlerTokenSource.IsCancellationRequested) - { - _waitForProbe.Wait (_inputHandlerTokenSource.Token); - } - } - catch (OperationCanceledException) - { - return; - } - finally - { - if (_waitForProbe.IsSet) - { - _waitForProbe.Reset (); - } - } - - if (_inputHandlerTokenSource.IsCancellationRequested) - { - return; - } - - _inputHandlerTokenSource.Token.ThrowIfCancellationRequested (); - - if (_resultQueue.Count == 0) - { - _resultQueue.Enqueue (_netEvents.DequeueInput ()); - } - - while (_resultQueue.Count > 0 && _resultQueue.Peek () is null) - { - _resultQueue.Dequeue (); - } - - if (_resultQueue.Count > 0) - { - _eventReady.Set (); - } - } - } -} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs new file mode 100644 index 0000000000..62c6db94b8 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -0,0 +1,790 @@ +#nullable enable +// +// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient. +// + +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Terminal.Gui.NetEvents; + +namespace Terminal.Gui; + +internal class NetDriver : ConsoleDriver +{ + public bool IsWinPlatform { get; private set; } + public NetWinVTConsole? NetWinConsole { get; private set; } + + public override void Suspend () + { + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + return; + } + + StopReportingMouseMoves (); + + if (!RunningUnitTests) + { + Console.ResetColor (); + Console.Clear (); + + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + + //Set cursor key to cursor. + Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + + Platform.Suspend (); + + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + + SetContentsAsDirty (); + Refresh (); + } + + StartReportingMouseMoves (); + } + + public override bool UpdateScreen () + { + bool updated = false; + if (RunningUnitTests + || _winSizeChanging + || Console.WindowHeight < 1 + || Contents?.Length != Rows * Cols + || Rows != Console.WindowHeight) + { + return updated; + } + + var top = 0; + var left = 0; + int rows = Rows; + int cols = Cols; + var output = new StringBuilder (); + Attribute? redrawAttr = null; + int lastCol = -1; + + CursorVisibility? savedVisibility = _cachedCursorVisibility; + SetCursorVisibility (CursorVisibility.Invisible); + + for (int row = top; row < rows; row++) + { + if (Console.WindowHeight < 1) + { + return updated; + } + + if (!_dirtyLines! [row]) + { + continue; + } + + if (!SetCursorPosition (0, row)) + { + return updated; + } + + updated = true; + _dirtyLines [row] = false; + output.Clear (); + + for (int col = left; col < cols; col++) + { + lastCol = -1; + var outputWidth = 0; + + for (; col < cols; col++) + { + if (!Contents [row, col].IsDirty) + { + if (output.Length > 0) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (lastCol == -1) + { + lastCol = col; + } + + if (lastCol + 1 < cols) + { + lastCol++; + } + + continue; + } + + if (lastCol == -1) + { + lastCol = col; + } + + Attribute attr = Contents [row, col].Attribute!.Value; + + // Performance: Only send the escape sequence if the attribute has changed. + if (attr != redrawAttr) + { + redrawAttr = attr; + + if (Force16Colors) + { + output.Append ( + EscSeqUtils.CSI_SetGraphicsRendition ( + MapColors ( + (ConsoleColor)attr.Background + .GetClosestNamedColor16 (), + false + ), + MapColors ( + (ConsoleColor)attr.Foreground + .GetClosestNamedColor16 ()) + ) + ); + } + else + { + output.Append ( + EscSeqUtils.CSI_SetForegroundColorRGB ( + attr.Foreground.R, + attr.Foreground.G, + attr.Foreground.B + ) + ); + + output.Append ( + EscSeqUtils.CSI_SetBackgroundColorRGB ( + attr.Background.R, + attr.Background.G, + attr.Background.B + ) + ); + } + } + + outputWidth++; + Rune rune = Contents [row, col].Rune; + output.Append (rune); + + if (Contents [row, col].CombiningMarks.Count > 0) + { + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. + // + // For now, we just ignore the list of CMs. + //foreach (var combMark in Contents [row, col].CombiningMarks) { + // output.Append (combMark); + //} + // WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + SetCursorPosition (col - 1, row); + } + + Contents [row, col].IsDirty = false; + } + } + + if (output.Length > 0) + { + SetCursorPosition (lastCol, row); + Console.Write (output); + } + + foreach (var s in Application.Sixel) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y); + Console.Write (s.SixelData); + } + } + } + + SetCursorPosition (0, 0); + + _cachedCursorVisibility = savedVisibility; + + void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) + { + SetCursorPosition (lastCol, row); + Console.Write (output); + output.Clear (); + lastCol += outputWidth; + outputWidth = 0; + } + + return updated; + } + + #region Init/End/MainLoop + + internal NetMainLoop? _mainLoopDriver; + + public 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 = EscSeqUtils.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 (); + } + } + + public override void End () + { + if (IsWinPlatform) + { + NetWinConsole?.Cleanup (); + } + + StopReportingMouseMoves (); + + if (!RunningUnitTests) + { + Console.ResetColor (); + + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + + //Set cursor key to cursor. + Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + Console.Out.Close (); + } + } + + #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); + //} + + #endregion Keyboard Handling + + #region Low-Level DotNet tuff + + /// + public override void WriteRaw (string ansi) + { + Console.Out.Write (ansi); + Console.Out.Flush (); + } + + private volatile bool _winSizeChanging; + + private void SetWindowPosition (int col, int row) + { + if (!RunningUnitTests) + { + Top = Console.WindowTop; + Left = Console.WindowLeft; + } + else + { + Top = row; + Left = col; + } + } + + public virtual void ResizeScreen () + { + // Not supported on Unix. + if (IsWinPlatform) + { + // Can raise an exception while is still resizing. + try + { +#pragma warning disable CA1416 + if (Console.WindowHeight > 0) + { + Console.CursorTop = 0; + Console.CursorLeft = 0; + Console.WindowTop = 0; + Console.WindowLeft = 0; + + if (Console.WindowHeight > Rows) + { + Console.SetWindowSize (Cols, Rows); + } + + Console.SetBufferSize (Cols, Rows); + } +#pragma warning restore CA1416 + } + // INTENT: Why are these eating the exceptions? + // Comments would be good here. + catch (IOException) + { + // CONCURRENCY: Unsynchronized access to Clip is not safe. + Clip = new (Screen); + } + catch (ArgumentOutOfRangeException) + { + // CONCURRENCY: Unsynchronized access to Clip is not safe. + Clip = new (Screen); + } + } + else + { + Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols)); + } + + // CONCURRENCY: Unsynchronized access to Clip is not safe. + Clip = new (Screen); + } + + #endregion Low-Level DotNet tuff +} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs new file mode 100644 index 0000000000..9ba56d673e --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs @@ -0,0 +1,695 @@ +#nullable enable +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +internal class NetEvents : IDisposable +{ + private readonly ManualResetEventSlim _inputReady = new (false); + private CancellationTokenSource? _inputReadyCancellationTokenSource; + private readonly Queue _inputQueue = new (); + private readonly IConsoleDriver _consoleDriver; + private ConsoleKeyInfo []? _cki; + private bool _isEscSeq; +#if PROCESS_REQUEST + bool _neededProcessRequest; +#endif + public NetEvents (IConsoleDriver consoleDriver) + { + _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); + _inputReadyCancellationTokenSource = new (); + + Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); + + Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); + } + + public InputResult? DequeueInput () + { + while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false }) + { + try + { + if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) + { + if (_inputQueue.Count == 0) + { + _inputReady.Wait (_inputReadyCancellationTokenSource.Token); + } + } + + if (_inputQueue.Count > 0) + { + return _inputQueue.Dequeue (); + } + } + catch (OperationCanceledException) + { + return null; + } + finally + { + if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + { + _inputReady.Reset (); + } + } + +#if PROCESS_REQUEST + _neededProcessRequest = false; +#endif + } + + return null; + } + + private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) + { + while (!cancellationToken.IsCancellationRequested) + { + // if there is a key available, return it without waiting + // (or dispatching work to the thread queue) + if (Console.KeyAvailable) + { + return Console.ReadKey (intercept); + } + + // The delay must be here because it may have a request response after a while + // In WSL it takes longer for keys to be available. + Task.Delay (100, cancellationToken).Wait (cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested (); + + return default (ConsoleKeyInfo); + } + + private void ProcessInputQueue () + { + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + { + try + { + ConsoleKey key = 0; + ConsoleModifiers mod = 0; + ConsoleKeyInfo newConsoleKeyInfo = default; + + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + { + ConsoleKeyInfo consoleKeyInfo; + + try + { + consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + return; + } + + var ckiAlreadyResized = false; + + if (EscSeqUtils.IncompleteCkInfos is { }) + { + ckiAlreadyResized = true; + + _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); + _cki = EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki); + EscSeqUtils.IncompleteCkInfos = null; + + if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B') + { + _isEscSeq = true; + } + } + + if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) + || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) + { + if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) + { + _cki = EscSeqUtils.ResizeArray ( + new ( + (char)KeyCode.Esc, + 0, + false, + false, + false + ), + _cki + ); + } + + _isEscSeq = true; + + if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space) + || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127) + || (_cki is { } + && char.IsLetter (_cki [^1].KeyChar) + && char.IsLower (consoleKeyInfo.KeyChar) + && char.IsLetter (consoleKeyInfo.KeyChar)) + || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetterOrDigit (consoleKeyInfo.KeyChar)) + || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar)) + || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar))) + { + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); + _cki = null; + _isEscSeq = false; + + ProcessMapConsoleKeyInfo (consoleKeyInfo); + } + else + { + newConsoleKeyInfo = consoleKeyInfo; + + if (!ckiAlreadyResized) + { + _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); + } + + if (Console.KeyAvailable) + { + continue; + } + + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki!, ref mod); + _cki = null; + _isEscSeq = false; + } + + break; + } + + if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { }) + { + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); + _cki = null; + + if (Console.KeyAvailable) + { + _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); + } + else + { + ProcessMapConsoleKeyInfo (consoleKeyInfo); + } + + break; + } + + ProcessMapConsoleKeyInfo (consoleKeyInfo); + + break; + } + + if (_inputQueue.Count > 0) + { + _inputReady.Set (); + } + } + catch (OperationCanceledException) + { + return; + } + } + + void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + _inputQueue.Enqueue ( + new () + { + 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 + { + RequestWindowSize (_inputReadyCancellationTokenSource.Token); + + if (_inputQueue.Count > 0) + { + _inputReady.Set (); + } + } + catch (OperationCanceledException) + { + return; + } + } + } + + /// Enqueue a window size event if the window size has changed. + /// + /// + /// + /// + /// + private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth) + { + if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows) + { + return false; + } + + int w = Math.Max (winWidth, 0); + int h = Math.Max (winHeight, 0); + + _inputQueue.Enqueue ( + new () + { + EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (w, h) } + } + ); + + return true; + } + + // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) + private void ProcessRequestResponse ( + ref ConsoleKeyInfo newConsoleKeyInfo, + ref ConsoleKey key, + ConsoleKeyInfo [] cki, + ref ConsoleModifiers mod + ) + { + // isMouse is true if it's CSI<, false otherwise + EscSeqUtils.DecodeEscSeq ( + ref newConsoleKeyInfo, + ref key, + cki, + ref mod, + out string c1Control, + out string code, + out string [] values, + out string terminating, + out bool isMouse, + out List mouseFlags, + out Point pos, + out bool isReq, + (f, p) => HandleMouseEvent (MapMouseFlags (f), p) + ); + + if (isMouse) + { + foreach (MouseFlags mf in mouseFlags) + { + HandleMouseEvent (MapMouseFlags (mf), pos); + } + + return; + } + + if (isReq) + { + HandleRequestResponseEvent (c1Control, code, values, terminating); + + return; + } + + 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_ResponseValue) + { + EnqueueWindowSizeEvent ( + Math.Max (int.Parse (values [1]), 0), + Math.Max (int.Parse (values [2]), 0), + Math.Max (int.Parse (values [1]), 0), + Math.Max (int.Parse (values [2]), 0) + ); + } + else + { + EnqueueRequestResponseEvent (c1Control, code, values, terminating); + } + } + else + { + EnqueueRequestResponseEvent (c1Control, code, values, terminating); + } + + _inputReady.Set (); + } + + private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating) + { + var eventType = EventType.RequestResponse; + var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) }; + + _inputQueue.Enqueue ( + new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv } + ); + } + + private void HandleMouseEvent (MouseButtonState buttonState, Point pos) + { + var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState }; + + _inputQueue.Enqueue ( + new () { EventType = EventType.Mouse, MouseEvent = mouseEvent } + ); + } + + public enum EventType + { + Key = 1, + Mouse = 2, + WindowSize = 3, + WindowPosition = 4, + RequestResponse = 5 + } + + [Flags] + public enum MouseButtonState + { + Button1Pressed = 0x1, + Button1Released = 0x2, + Button1Clicked = 0x4, + Button1DoubleClicked = 0x8, + Button1TripleClicked = 0x10, + Button2Pressed = 0x20, + Button2Released = 0x40, + Button2Clicked = 0x80, + Button2DoubleClicked = 0x100, + Button2TripleClicked = 0x200, + Button3Pressed = 0x400, + Button3Released = 0x800, + Button3Clicked = 0x1000, + Button3DoubleClicked = 0x2000, + Button3TripleClicked = 0x4000, + ButtonWheeledUp = 0x8000, + ButtonWheeledDown = 0x10000, + ButtonWheeledLeft = 0x20000, + ButtonWheeledRight = 0x40000, + Button4Pressed = 0x80000, + Button4Released = 0x100000, + Button4Clicked = 0x200000, + Button4DoubleClicked = 0x400000, + Button4TripleClicked = 0x800000, + ButtonShift = 0x1000000, + ButtonCtrl = 0x2000000, + ButtonAlt = 0x4000000, + ReportMousePosition = 0x8000000, + AllEvents = -1 + } + + public struct MouseEvent + { + public Point Position; + public MouseButtonState ButtonState; + } + + public struct WindowSizeEvent + { + public Size Size; + } + + public struct WindowPositionEvent + { + public int Top; + public int Left; + public Point CursorPosition; + } + + public struct RequestResponseEvent + { + public (string c1Control, string code, string [] values, string terminating) ResultTuple; + } + + public struct InputResult + { + public EventType EventType; + public ConsoleKeyInfo ConsoleKeyInfo; + public MouseEvent MouseEvent; + public WindowSizeEvent WindowSizeEvent; + public WindowPositionEvent WindowPositionEvent; + public RequestResponseEvent RequestResponseEvent; + + public readonly override string ToString () + { + return (EventType switch + { + EventType.Key => ToString (ConsoleKeyInfo), + EventType.Mouse => MouseEvent.ToString (), + + //EventType.WindowSize => WindowSize.ToString (), + //EventType.RequestResponse => RequestResponse.ToString (), + _ => "Unknown event type: " + EventType + })!; + } + + /// Prints a ConsoleKeyInfoEx structure + /// + /// + public readonly string ToString (ConsoleKeyInfo cki) + { + var ke = new Key ((KeyCode)cki.KeyChar); + var sb = new StringBuilder (); + sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})"); + sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); + sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); + sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); + sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) "); + string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); + + return $"[ConsoleKeyInfo({s})]"; + } + } + + private void HandleKeyboardEvent (ConsoleKeyInfo cki) + { + var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki }; + + _inputQueue.Enqueue (inputResult); + } + + public void Dispose () + { + _inputReadyCancellationTokenSource?.Cancel (); + _inputReadyCancellationTokenSource?.Dispose (); + _inputReadyCancellationTokenSource = null; + + _inputReady.Dispose (); + + try + { + // throws away any typeahead that has been typed by + // the user and has not yet been read by the program. + while (Console.KeyAvailable) + { + Console.ReadKey (true); + } + } + catch (InvalidOperationException) + { + // Ignore - Console input has already been closed + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs new file mode 100644 index 0000000000..32e7c53dbd --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs @@ -0,0 +1,168 @@ +#nullable enable + +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +/// +/// Mainloop intended to be used with the .NET System.Console API, and can be used on Windows and Unix, it is +/// cross-platform but lacks things like file descriptor monitoring. +/// +/// This implementation is used for NetDriver. +internal class NetMainLoop : IMainLoopDriver +{ + internal NetEvents? _netEvents; + + /// Invoked when a Key is pressed. + internal Action? ProcessInput; + + private readonly ManualResetEventSlim _eventReady = new (false); + private readonly CancellationTokenSource _eventReadyTokenSource = new (); + private readonly CancellationTokenSource _inputHandlerTokenSource = new (); + private readonly ManualResetEventSlim _waitForProbe = new (false); + private readonly ConcurrentQueue _resultQueue = new (); + private MainLoop? _mainLoop; + + /// Initializes the class with the console driver. + /// Passing a IConsoleDriver is provided to capture windows resizing. + /// The console driver used by this Net main loop. + /// + public NetMainLoop (IConsoleDriver consoleDriver) + { + ArgumentNullException.ThrowIfNull (consoleDriver); + + if (!ConsoleDriver.RunningUnitTests) + { + _netEvents = new (consoleDriver); + } + } + + void IMainLoopDriver.Setup (MainLoop mainLoop) + { + _mainLoop = mainLoop; + + if (!ConsoleDriver.RunningUnitTests) + { + Task.Run (NetInputHandler, _inputHandlerTokenSource.Token); + } + } + + void IMainLoopDriver.Wakeup () { _eventReady.Set (); } + + bool IMainLoopDriver.EventsPending () + { + if (ConsoleDriver.RunningUnitTests) + { + return true; + } + + _waitForProbe.Set (); + + if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout)) + { + return true; + } + + try + { + if (!_eventReadyTokenSource.IsCancellationRequested) + { + // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there + // are no timers, but there IS an idle handler waiting. + _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); + } + } + catch (OperationCanceledException) + { + return true; + } + finally + { + _eventReady.Reset (); + } + + _eventReadyTokenSource.Token.ThrowIfCancellationRequested (); + + if (!_eventReadyTokenSource.IsCancellationRequested) + { + return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); + } + + // If cancellation was requested then always return true + return true; + } + + void IMainLoopDriver.Iteration () + { + while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out NetEvents.InputResult inputRecords)) + { + ProcessInput?.Invoke (inputRecords); + } + } + + void IMainLoopDriver.TearDown () + { + _inputHandlerTokenSource.Cancel (); + _inputHandlerTokenSource.Dispose (); + _eventReadyTokenSource.Cancel (); + _eventReadyTokenSource.Dispose (); + + _eventReady.Dispose (); + _waitForProbe.Dispose (); + + _resultQueue.Clear (); + _netEvents?.Dispose (); + _netEvents = null; + + _mainLoop = null; + } + + private void NetInputHandler () + { + while (_mainLoop is { }) + { + try + { + if (!_inputHandlerTokenSource.IsCancellationRequested) + { + try + { + _waitForProbe.Wait (_inputHandlerTokenSource.Token); + } + catch (Exception ex) + { + if (ex is OperationCanceledException or ObjectDisposedException) + { + return; + } + + throw; + } + + _waitForProbe.Reset (); + } + + ProcessInputQueue (); + } + catch (OperationCanceledException) + { + return; + } + } + } + + private void ProcessInputQueue () + { + if (_resultQueue.Count == 0) + { + NetEvents.InputResult? result = _netEvents!.DequeueInput (); + + if (result.HasValue) + { + _resultQueue.Enqueue (result.Value); + + _eventReady.Set (); + } + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs new file mode 100644 index 0000000000..98666f4609 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetWinVTConsole.cs @@ -0,0 +1,126 @@ +#nullable enable +using System.Runtime.InteropServices; + +namespace Terminal.Gui; + +internal class NetWinVTConsole +{ + private const uint DISABLE_NEWLINE_AUTO_RETURN = 8; + private const uint ENABLE_ECHO_INPUT = 4; + private const uint ENABLE_EXTENDED_FLAGS = 128; + private const uint ENABLE_INSERT_MODE = 32; + private const uint ENABLE_LINE_INPUT = 2; + private const uint ENABLE_LVB_GRID_WORLDWIDE = 10; + private const uint ENABLE_MOUSE_INPUT = 16; + + // Input modes. + private const uint ENABLE_PROCESSED_INPUT = 1; + + // Output modes. + private const uint ENABLE_PROCESSED_OUTPUT = 1; + private const uint ENABLE_QUICK_EDIT_MODE = 64; + private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512; + private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4; + private const uint ENABLE_WINDOW_INPUT = 8; + private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2; + private const int STD_ERROR_HANDLE = -12; + private const int STD_INPUT_HANDLE = -10; + private const int STD_OUTPUT_HANDLE = -11; + + private readonly nint _errorHandle; + private readonly nint _inputHandle; + private readonly uint _originalErrorConsoleMode; + private readonly uint _originalInputConsoleMode; + private readonly uint _originalOutputConsoleMode; + private readonly nint _outputHandle; + + public NetWinVTConsole () + { + _inputHandle = GetStdHandle (STD_INPUT_HANDLE); + + if (!GetConsoleMode (_inputHandle, out uint mode)) + { + throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}."); + } + + _originalInputConsoleMode = mode; + + if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT) + { + mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + + if (!SetConsoleMode (_inputHandle, mode)) + { + throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}."); + } + } + + _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); + + if (!GetConsoleMode (_outputHandle, out mode)) + { + throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}."); + } + + _originalOutputConsoleMode = mode; + + if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN) + { + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; + + if (!SetConsoleMode (_outputHandle, mode)) + { + throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}."); + } + } + + _errorHandle = GetStdHandle (STD_ERROR_HANDLE); + + if (!GetConsoleMode (_errorHandle, out mode)) + { + throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}."); + } + + _originalErrorConsoleMode = mode; + + if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN) + { + mode |= DISABLE_NEWLINE_AUTO_RETURN; + + if (!SetConsoleMode (_errorHandle, mode)) + { + throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}."); + } + } + } + + public void Cleanup () + { + if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode)) + { + throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}."); + } + + if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode)) + { + throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}."); + } + + if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode)) + { + throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}."); + } + } + + [DllImport ("kernel32.dll")] + private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); + + [DllImport ("kernel32.dll")] + private static extern uint GetLastError (); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint GetStdHandle (int nStdHandle); + + [DllImport ("kernel32.dll")] + private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs deleted file mode 100644 index 653b946818..0000000000 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ /dev/null @@ -1,2545 +0,0 @@ -// -// WindowsDriver.cs: Windows specific driver -// - -// HACK: -// WindowsConsole/Terminal has two issues: -// 1) Tearing can occur when the console is resized. -// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct. -// -// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events -// and instead check the console size every every 500ms in a thread in WidowsMainLoop. -// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using -// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is -// still incorrect so we still need this hack. - -#define HACK_CHECK_WINCHANGED - -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; -using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; -using static Terminal.Gui.SpinnerStyle; - -namespace Terminal.Gui; - -internal class WindowsConsole -{ - public const int STD_OUTPUT_HANDLE = -11; - public const int STD_INPUT_HANDLE = -10; - - private readonly nint _inputHandle; - private readonly nint _outputHandle; - private nint _screenBuffer; - private readonly uint _originalConsoleMode; - private CursorVisibility? _initialCursorVisibility; - private CursorVisibility? _currentCursorVisibility; - private CursorVisibility? _pendingCursorVisibility; - private readonly StringBuilder _stringBuilder = new (256 * 1024); - private string _lastWrite = string.Empty; - - public WindowsConsole () - { - _inputHandle = GetStdHandle (STD_INPUT_HANDLE); - _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); - _originalConsoleMode = ConsoleMode; - uint newConsoleMode = _originalConsoleMode; - newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); - newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; - newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; - ConsoleMode = newConsoleMode; - } - - private CharInfo [] _originalStdOutChars; - - public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) - { - //Debug.WriteLine ("WriteToConsole"); - - if (_screenBuffer == nint.Zero) - { - ReadFromConsoleOutput (size, bufferSize, ref window); - } - - var result = false; - - if (force16Colors) - { - var i = 0; - CharInfo [] ci = new CharInfo [charInfoBuffer.Length]; - - foreach (ExtendedCharInfo info in charInfoBuffer) - { - ci [i++] = new CharInfo - { - Char = new CharUnion { UnicodeChar = info.Char }, - Attributes = - (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) - }; - } - - result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window); - } - else - { - _stringBuilder.Clear (); - - _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); - - Attribute? prev = null; - - foreach (ExtendedCharInfo info in charInfoBuffer) - { - Attribute attr = info.Attribute; - - if (attr != prev) - { - prev = attr; - _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); - _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); - } - - if (info.Char != '\x1b') - { - if (!info.Empty) - { - _stringBuilder.Append (info.Char); - } - } - else - { - _stringBuilder.Append (' '); - } - } - - _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); - _stringBuilder.Append (EscSeqUtils.CSI_HideCursor); - - var s = _stringBuilder.ToString (); - - // TODO: requires extensive testing if we go down this route - // If console output has changed - if (s != _lastWrite) - { - // supply console with the new content - result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero); - } - - _lastWrite = s; - - foreach (var sixel in Application.Sixel) - { - SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y)); - WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); - } - } - - if (!result) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) - { - throw new Win32Exception (err); - } - } - - return result; - } - - public bool WriteANSI (string ansi) - { - return WriteConsole (_screenBuffer, ansi, (uint)ansi.Length, out uint _, nint.Zero); - } - - public void ReadFromConsoleOutput (Size size, Coord coords, ref SmallRect window) - { - _screenBuffer = CreateConsoleScreenBuffer ( - DesiredAccess.GenericRead | DesiredAccess.GenericWrite, - ShareMode.FileShareRead | ShareMode.FileShareWrite, - nint.Zero, - 1, - nint.Zero - ); - - if (_screenBuffer == INVALID_HANDLE_VALUE) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) - { - throw new Win32Exception (err); - } - } - - SetInitialCursorVisibility (); - - if (!SetConsoleActiveScreenBuffer (_screenBuffer)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - _originalStdOutChars = new CharInfo [size.Height * size.Width]; - - if (!ReadConsoleOutput (_screenBuffer, _originalStdOutChars, coords, new Coord { X = 0, Y = 0 }, ref window)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - } - - public bool SetCursorPosition (Coord position) - { - return SetConsoleCursorPosition (_screenBuffer, position); - } - - public void SetInitialCursorVisibility () - { - if (_initialCursorVisibility.HasValue == false && GetCursorVisibility (out CursorVisibility visibility)) - { - _initialCursorVisibility = visibility; - } - } - - public bool GetCursorVisibility (out CursorVisibility visibility) - { - if (_screenBuffer == nint.Zero) - { - visibility = CursorVisibility.Invisible; - - return false; - } - - if (!GetConsoleCursorInfo (_screenBuffer, out ConsoleCursorInfo info)) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) - { - throw new Win32Exception (err); - } - - visibility = CursorVisibility.Default; - - return false; - } - - if (!info.bVisible) - { - visibility = CursorVisibility.Invisible; - } - else if (info.dwSize > 50) - { - visibility = CursorVisibility.Default; - } - else - { - visibility = CursorVisibility.Default; - } - - return true; - } - - public bool EnsureCursorVisibility () - { - if (_initialCursorVisibility.HasValue && _pendingCursorVisibility.HasValue && SetCursorVisibility (_pendingCursorVisibility.Value)) - { - _pendingCursorVisibility = null; - - return true; - } - - return false; - } - - public void ForceRefreshCursorVisibility () - { - if (_currentCursorVisibility.HasValue) - { - _pendingCursorVisibility = _currentCursorVisibility; - _currentCursorVisibility = null; - } - } - - public bool SetCursorVisibility (CursorVisibility visibility) - { - if (_initialCursorVisibility.HasValue == false) - { - _pendingCursorVisibility = visibility; - - return false; - } - - if (_currentCursorVisibility.HasValue == false || _currentCursorVisibility.Value != visibility) - { - var info = new ConsoleCursorInfo - { - dwSize = (uint)visibility & 0x00FF, - bVisible = ((uint)visibility & 0xFF00) != 0 - }; - - if (!SetConsoleCursorInfo (_screenBuffer, ref info)) - { - return false; - } - - _currentCursorVisibility = visibility; - } - - return true; - } - - public void Cleanup () - { - if (_initialCursorVisibility.HasValue) - { - SetCursorVisibility (_initialCursorVisibility.Value); - } - - SetConsoleOutputWindow (out _); - - ConsoleMode = _originalConsoleMode; - - if (!SetConsoleActiveScreenBuffer (_outputHandle)) - { - int err = Marshal.GetLastWin32Error (); - Console.WriteLine ("Error: {0}", err); - } - - if (_screenBuffer != nint.Zero) - { - CloseHandle (_screenBuffer); - } - - _screenBuffer = nint.Zero; - } - - internal Size GetConsoleBufferWindow (out Point position) - { - if (_screenBuffer == nint.Zero) - { - position = Point.Empty; - - return Size.Empty; - } - - var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); - csbi.cbSize = (uint)Marshal.SizeOf (csbi); - - if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) - { - //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); - position = Point.Empty; - - return Size.Empty; - } - - Size sz = new ( - csbi.srWindow.Right - csbi.srWindow.Left + 1, - csbi.srWindow.Bottom - csbi.srWindow.Top + 1); - position = new (csbi.srWindow.Left, csbi.srWindow.Top); - - return sz; - } - - internal Size GetConsoleOutputWindow (out Point position) - { - var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); - csbi.cbSize = (uint)Marshal.SizeOf (csbi); - - if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - Size sz = new ( - csbi.srWindow.Right - csbi.srWindow.Left + 1, - csbi.srWindow.Bottom - csbi.srWindow.Top + 1); - position = new (csbi.srWindow.Left, csbi.srWindow.Top); - - return sz; - } - - internal Size SetConsoleWindow (short cols, short rows) - { - var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); - csbi.cbSize = (uint)Marshal.SizeOf (csbi); - - if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer); - short newCols = Math.Min (cols, maxWinSize.X); - short newRows = Math.Min (rows, maxWinSize.Y); - csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1)); - csbi.srWindow = new SmallRect (0, 0, newCols, newRows); - csbi.dwMaximumWindowSize = new Coord (newCols, newRows); - - if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0)); - - if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) - { - //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); - return new (cols, rows); - } - - SetConsoleOutputWindow (csbi); - - return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1); - } - - private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi) - { - if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - } - - internal Size SetConsoleOutputWindow (out Point position) - { - if (_screenBuffer == nint.Zero) - { - position = Point.Empty; - - return Size.Empty; - } - - var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); - csbi.cbSize = (uint)Marshal.SizeOf (csbi); - - if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - Size sz = new ( - csbi.srWindow.Right - csbi.srWindow.Left + 1, - Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0)); - position = new (csbi.srWindow.Left, csbi.srWindow.Top); - SetConsoleOutputWindow (csbi); - var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0)); - - if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } - - return sz; - } - - private uint ConsoleMode - { - get - { - GetConsoleMode (_inputHandle, out uint v); - - return v; - } - set => SetConsoleMode (_inputHandle, value); - } - - [Flags] - public enum ConsoleModes : uint - { - EnableProcessedInput = 1, - EnableMouseInput = 16, - EnableQuickEditMode = 64, - EnableExtendedFlags = 128 - } - - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct KeyEventRecord - { - [FieldOffset (0)] - [MarshalAs (UnmanagedType.Bool)] - public bool bKeyDown; - - [FieldOffset (4)] - [MarshalAs (UnmanagedType.U2)] - public ushort wRepeatCount; - - [FieldOffset (6)] - [MarshalAs (UnmanagedType.U2)] - public VK wVirtualKeyCode; - - [FieldOffset (8)] - [MarshalAs (UnmanagedType.U2)] - public ushort wVirtualScanCode; - - [FieldOffset (10)] - public char UnicodeChar; - - [FieldOffset (12)] - [MarshalAs (UnmanagedType.U4)] - public ControlKeyState dwControlKeyState; - - public readonly override string ToString () - { - return - $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]"; - } - } - - [Flags] - public enum ButtonState - { - NoButtonPressed = 0, - Button1Pressed = 1, - Button2Pressed = 4, - Button3Pressed = 8, - Button4Pressed = 16, - RightmostButtonPressed = 2 - } - - [Flags] - public enum ControlKeyState - { - NoControlKeyPressed = 0, - RightAltPressed = 1, - LeftAltPressed = 2, - RightControlPressed = 4, - LeftControlPressed = 8, - ShiftPressed = 16, - NumlockOn = 32, - ScrolllockOn = 64, - CapslockOn = 128, - EnhancedKey = 256 - } - - [Flags] - public enum EventFlags - { - NoEvent = 0, - MouseMoved = 1, - DoubleClick = 2, - MouseWheeled = 4, - MouseHorizontalWheeled = 8 - } - - [StructLayout (LayoutKind.Explicit)] - public struct MouseEventRecord - { - [FieldOffset (0)] - public Coord MousePosition; - - [FieldOffset (4)] - public ButtonState ButtonState; - - [FieldOffset (8)] - public ControlKeyState ControlKeyState; - - [FieldOffset (12)] - public EventFlags EventFlags; - - public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; } - } - - public struct WindowBufferSizeRecord - { - public Coord _size; - - public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); } - - public readonly override string ToString () { return $"[WindowBufferSize{_size}"; } - } - - [StructLayout (LayoutKind.Sequential)] - public struct MenuEventRecord - { - public uint dwCommandId; - } - - [StructLayout (LayoutKind.Sequential)] - public struct FocusEventRecord - { - public uint bSetFocus; - } - - public enum EventType : ushort - { - Focus = 0x10, - Key = 0x1, - Menu = 0x8, - Mouse = 2, - WindowBufferSize = 4 - } - - [StructLayout (LayoutKind.Explicit)] - public struct InputRecord - { - [FieldOffset (0)] - public EventType EventType; - - [FieldOffset (4)] - public KeyEventRecord KeyEvent; - - [FieldOffset (4)] - public MouseEventRecord MouseEvent; - - [FieldOffset (4)] - public WindowBufferSizeRecord WindowBufferSizeEvent; - - [FieldOffset (4)] - public MenuEventRecord MenuEvent; - - [FieldOffset (4)] - public FocusEventRecord FocusEvent; - - public readonly override string ToString () - { - return EventType switch - { - EventType.Focus => FocusEvent.ToString (), - EventType.Key => KeyEvent.ToString (), - EventType.Menu => MenuEvent.ToString (), - EventType.Mouse => MouseEvent.ToString (), - EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), - _ => "Unknown event type: " + EventType - }; - } - } - - [Flags] - private enum ShareMode : uint - { - FileShareRead = 1, - FileShareWrite = 2 - } - - [Flags] - private enum DesiredAccess : uint - { - GenericRead = 2147483648, - GenericWrite = 1073741824 - } - - [StructLayout (LayoutKind.Sequential)] - public struct ConsoleScreenBufferInfo - { - public Coord dwSize; - public Coord dwCursorPosition; - public ushort wAttributes; - public SmallRect srWindow; - public Coord dwMaximumWindowSize; - } - - [StructLayout (LayoutKind.Sequential)] - public struct Coord - { - public short X; - public short Y; - - public Coord (short x, short y) - { - X = x; - Y = y; - } - - public readonly override string ToString () { return $"({X},{Y})"; } - } - - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct CharUnion - { - [FieldOffset (0)] - public char UnicodeChar; - - [FieldOffset (0)] - public byte AsciiChar; - } - - [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] - public struct CharInfo - { - [FieldOffset (0)] - public CharUnion Char; - - [FieldOffset (2)] - public ushort Attributes; - } - - public struct ExtendedCharInfo - { - public char Char { get; set; } - public Attribute Attribute { get; set; } - public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences - - public ExtendedCharInfo (char character, Attribute attribute) - { - Char = character; - Attribute = attribute; - Empty = false; - } - } - - [StructLayout (LayoutKind.Sequential)] - public struct SmallRect - { - public short Left; - public short Top; - public short Right; - public short Bottom; - - public SmallRect (short left, short top, short right, short bottom) - { - Left = left; - Top = top; - Right = right; - Bottom = bottom; - } - - public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; } - - public static void Update (ref SmallRect rect, short col, short row) - { - if (rect.Left == -1) - { - rect.Left = rect.Right = col; - rect.Bottom = rect.Top = row; - - return; - } - - if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom) - { - return; - } - - if (col < rect.Left) - { - rect.Left = col; - } - - if (col > rect.Right) - { - rect.Right = col; - } - - if (row < rect.Top) - { - rect.Top = row; - } - - if (row > rect.Bottom) - { - rect.Bottom = row; - } - } - - public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; } - } - - [StructLayout (LayoutKind.Sequential)] - public struct ConsoleKeyInfoEx - { - public ConsoleKeyInfo ConsoleKeyInfo; - public bool CapsLock; - public bool NumLock; - public bool ScrollLock; - - public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock) - { - ConsoleKeyInfo = consoleKeyInfo; - CapsLock = capslock; - NumLock = numlock; - ScrollLock = scrolllock; - } - - /// - /// Prints a ConsoleKeyInfoEx structure - /// - /// - /// - public readonly string ToString (ConsoleKeyInfoEx ex) - { - var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar); - var sb = new StringBuilder (); - sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})"); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); - sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); - sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) "); - sb.Append (ex.CapsLock ? "caps," : string.Empty); - sb.Append (ex.NumLock ? "num," : string.Empty); - sb.Append (ex.ScrollLock ? "scroll," : string.Empty); - string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); - - return $"[ConsoleKeyInfoEx({s})]"; - } - } - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint GetStdHandle (int nStdHandle); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool CloseHandle (nint handle); - - [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] - public static extern bool ReadConsoleInput ( - nint hConsoleInput, - nint lpBuffer, - uint nLength, - out uint lpNumberOfEventsRead - ); - - [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool ReadConsoleOutput ( - nint hConsoleOutput, - [Out] CharInfo [] lpBuffer, - Coord dwBufferSize, - Coord dwBufferCoord, - ref SmallRect lpReadRegion - ); - - // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput - [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WriteConsoleOutput ( - nint hConsoleOutput, - CharInfo [] lpBuffer, - Coord dwBufferSize, - Coord dwBufferCoord, - ref SmallRect lpWriteRegion - ); - - [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WriteConsole ( - nint hConsoleOutput, - string lpbufer, - uint NumberOfCharsToWriten, - out uint lpNumberOfCharsWritten, - nint lpReserved - ); - - [DllImport ("kernel32.dll")] - private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition); - - [StructLayout (LayoutKind.Sequential)] - public struct ConsoleCursorInfo - { - /// - /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100. - /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal - /// line at the bottom of the cell. - /// - public uint dwSize; - public bool bVisible; - } - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo); - - [DllImport ("kernel32.dll")] - private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); - - [DllImport ("kernel32.dll")] - private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint CreateConsoleScreenBuffer ( - DesiredAccess dwDesiredAccess, - ShareMode dwShareMode, - nint secutiryAttributes, - uint flags, - nint screenBufferData - ); - - internal static nint INVALID_HANDLE_VALUE = new (-1); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleActiveScreenBuffer (nint Handle); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents); - - internal uint GetNumberOfConsoleInputEvents () - { - if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents)) - { - Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}"); - - return 0; - } - - return numOfEvents; - } - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool FlushConsoleInputBuffer (nint handle); - - internal void FlushConsoleInputBuffer () - { - if (!FlushConsoleInputBuffer (_inputHandle)) - { - Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}"); - } - } - - public InputRecord [] ReadConsoleInput () - { - const int bufferSize = 1; - nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); - - try - { - ReadConsoleInput ( - _inputHandle, - pRecord, - bufferSize, - out uint numberEventsRead); - - return numberEventsRead == 0 - ? null - : new [] { Marshal.PtrToStructure (pRecord) }; - } - catch (Exception) - { - return null; - } - finally - { - Marshal.FreeHGlobal (pRecord); - } - } - -#if false // Not needed on the constructor. Perhaps could be used on resizing. To study. - [DllImport ("kernel32.dll", ExactSpelling = true)] - static extern IntPtr GetConsoleWindow (); - - [DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] - static extern bool ShowWindow (IntPtr hWnd, int nCmdShow); - - public const int HIDE = 0; - public const int MAXIMIZE = 3; - public const int MINIMIZE = 6; - public const int RESTORE = 9; - - internal void ShowWindow (int state) - { - IntPtr thisConsole = GetConsoleWindow (); - ShowWindow (thisConsole, state); - } -#endif - - // See: https://github.com/gui-cs/Terminal.Gui/issues/357 - - [StructLayout (LayoutKind.Sequential)] - public struct CONSOLE_SCREEN_BUFFER_INFOEX - { - public uint cbSize; - public Coord dwSize; - public Coord dwCursorPosition; - public ushort wAttributes; - public SmallRect srWindow; - public Coord dwMaximumWindowSize; - public ushort wPopupAttributes; - public bool bFullscreenSupported; - - [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)] - public COLORREF [] ColorTable; - } - - [StructLayout (LayoutKind.Explicit, Size = 4)] - public struct COLORREF - { - public COLORREF (byte r, byte g, byte b) - { - Value = 0; - R = r; - G = g; - B = b; - } - - public COLORREF (uint value) - { - R = 0; - G = 0; - B = 0; - Value = value & 0x00FFFFFF; - } - - [FieldOffset (0)] - public byte R; - - [FieldOffset (1)] - public byte G; - - [FieldOffset (2)] - public byte B; - - [FieldOffset (0)] - public uint Value; - } - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleWindowInfo ( - nint hConsoleOutput, - bool bAbsolute, - [In] ref SmallRect lpConsoleWindow - ); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern Coord GetLargestConsoleWindowSize ( - nint hConsoleOutput - ); -} - -internal class WindowsDriver : ConsoleDriver -{ - private readonly bool _isWindowsTerminal; - - private WindowsConsole.SmallRect _damageRegion; - private bool _isButtonDoubleClicked; - private bool _isButtonPressed; - private bool _isButtonReleased; - private bool _isOneFingerDoubleClicked; - - private WindowsConsole.ButtonState? _lastMouseButtonPressed; - private WindowsMainLoop _mainLoopDriver; - private WindowsConsole.ExtendedCharInfo [] _outputBuffer; - private Point? _point; - private Point _pointMove; - private bool _processButtonClick; - - public WindowsDriver () - { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - WinConsole = new WindowsConsole (); - - // otherwise we're probably running in unit tests - Clipboard = new WindowsClipboard (); - } - else - { - Clipboard = new FakeDriver.FakeClipboard (); - } - - // TODO: if some other Windows-based terminal supports true color, update this logic to not - // force 16color mode (.e.g ConEmu which really doesn't work well at all). - _isWindowsTerminal = _isWindowsTerminal = - Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null; - - if (!_isWindowsTerminal) - { - Force16Colors = true; - } - } - - public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal); - - public WindowsConsole WinConsole { get; private set; } - - public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) - { - if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet) - { - return keyEvent; - } - - var mod = new ConsoleModifiers (); - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) - { - mod |= ConsoleModifiers.Shift; - } - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) - || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) - { - mod |= ConsoleModifiers.Alt; - } - - if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed) - || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) - { - mod |= ConsoleModifiers.Control; - } - - var cKeyInfo = new ConsoleKeyInfo ( - keyEvent.UnicodeChar, - (ConsoleKey)keyEvent.wVirtualKeyCode, - mod.HasFlag (ConsoleModifiers.Shift), - mod.HasFlag (ConsoleModifiers.Alt), - mod.HasFlag (ConsoleModifiers.Control)); - cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo); - uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo); - - return new WindowsConsole.KeyEventRecord - { - UnicodeChar = cKeyInfo.KeyChar, - bKeyDown = keyEvent.bKeyDown, - dwControlKeyState = keyEvent.dwControlKeyState, - wRepeatCount = keyEvent.wRepeatCount, - wVirtualKeyCode = (VK)cKeyInfo.Key, - wVirtualScanCode = (ushort)scanCode - }; - } - - public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; } - - public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) - { - var input = new WindowsConsole.InputRecord - { - EventType = WindowsConsole.EventType.Key - }; - - var keyEvent = new WindowsConsole.KeyEventRecord - { - bKeyDown = true - }; - var controlKey = new WindowsConsole.ControlKeyState (); - - if (shift) - { - controlKey |= WindowsConsole.ControlKeyState.ShiftPressed; - keyEvent.UnicodeChar = '\0'; - keyEvent.wVirtualKeyCode = VK.SHIFT; - } - - if (alt) - { - controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed; - controlKey |= WindowsConsole.ControlKeyState.RightAltPressed; - keyEvent.UnicodeChar = '\0'; - keyEvent.wVirtualKeyCode = VK.MENU; - } - - if (control) - { - controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed; - controlKey |= WindowsConsole.ControlKeyState.RightControlPressed; - keyEvent.UnicodeChar = '\0'; - keyEvent.wVirtualKeyCode = VK.CONTROL; - } - - keyEvent.dwControlKeyState = controlKey; - - input.KeyEvent = keyEvent; - - if (shift || alt || control) - { - ProcessInput (input); - } - - keyEvent.UnicodeChar = keyChar; - - //if ((uint)key < 255) { - // keyEvent.wVirtualKeyCode = (ushort)key; - //} else { - // keyEvent.wVirtualKeyCode = '\0'; - //} - keyEvent.wVirtualKeyCode = (VK)key; - - input.KeyEvent = keyEvent; - - try - { - ProcessInput (input); - } - catch (OverflowException) - { } - finally - { - keyEvent.bKeyDown = false; - input.KeyEvent = keyEvent; - ProcessInput (input); - } - } - - - #region Not Implemented - - public override void Suspend () { throw new NotImplementedException (); } - - #endregion - - public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) - { - WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState; - - bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; - bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; - bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; - bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0; - bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0; - bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0; - - var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - - return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock); - } - - #region Cursor Handling - - private CursorVisibility? _cachedCursorVisibility; - - public override void UpdateCursor () - { - if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows) - { - GetCursorVisibility (out CursorVisibility cursorVisibility); - _cachedCursorVisibility = cursorVisibility; - SetCursorVisibility (CursorVisibility.Invisible); - - return; - } - - var position = new WindowsConsole.Coord - { - X = (short)Col, - Y = (short)Row - }; - - if (Force16Colors) - { - WinConsole?.SetCursorPosition (position); - } - else - { - var sb = new StringBuilder (); - sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1)); - WinConsole?.WriteANSI (sb.ToString ()); - } - - if (_cachedCursorVisibility is { }) - { - SetCursorVisibility (_cachedCursorVisibility.Value); - } - //EnsureCursorVisibility (); - } - - /// - public override bool GetCursorVisibility (out CursorVisibility visibility) - { - if (WinConsole is { }) - { - return WinConsole.GetCursorVisibility (out visibility); - } - - visibility = _cachedCursorVisibility ?? CursorVisibility.Default; - - return true; - } - - /// - public override bool SetCursorVisibility (CursorVisibility visibility) - { - _cachedCursorVisibility = visibility; - - if (Force16Colors) - { - return WinConsole is null || WinConsole.SetCursorVisibility (visibility); - } - else - { - var sb = new StringBuilder (); - sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - return WinConsole?.WriteANSI (sb.ToString ()) ?? false; - } - } - - /// - public override bool EnsureCursorVisibility () - { - if (Force16Colors) - { - return WinConsole is null || WinConsole.EnsureCursorVisibility (); - } - else - { - var sb = new StringBuilder (); - sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - return WinConsole?.WriteANSI (sb.ToString ()) ?? false; - } - - //if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) - //{ - // GetCursorVisibility (out CursorVisibility cursorVisibility); - // _cachedCursorVisibility = cursorVisibility; - // SetCursorVisibility (CursorVisibility.Invisible); - - // return false; - //} - - //SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default); - - //return _cachedCursorVisibility == CursorVisibility.Default; - } - - #endregion Cursor Handling - - public override bool UpdateScreen () - { - bool updated = false; - Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows); - - if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows)) - { - return updated; - } - - var bufferCoords = new WindowsConsole.Coord - { - X = (short)Cols, //Clip.Width, - Y = (short)Rows, //Clip.Height - }; - - for (var row = 0; row < Rows; row++) - { - if (!_dirtyLines [row]) - { - continue; - } - - _dirtyLines [row] = false; - updated = true; - - for (var col = 0; col < Cols; col++) - { - int position = row * Cols + col; - _outputBuffer [position].Attribute = Contents [row, col].Attribute.GetValueOrDefault (); - - if (Contents [row, col].IsDirty == false) - { - _outputBuffer [position].Empty = true; - _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - - continue; - } - - _outputBuffer [position].Empty = false; - - if (Contents [row, col].Rune.IsBmp) - { - _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value; - } - else - { - //_outputBuffer [position].Empty = true; - _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - - if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols) - { - // TODO: This is a hack to deal with non-BMP and wide characters. - col++; - position = row * Cols + col; - _outputBuffer [position].Empty = false; - _outputBuffer [position].Char = ' '; - } - } - } - } - - _damageRegion = new WindowsConsole.SmallRect - { - Top = 0, - Left = 0, - Bottom = (short)Rows, - Right = (short)Cols - }; - - if (!RunningUnitTests - && WinConsole != null - && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors)) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) - { - throw new Win32Exception (err); - } - } - - WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); - - return updated; - } - - internal override void End () - { - if (_mainLoopDriver is { }) - { -#if HACK_CHECK_WINCHANGED - - _mainLoopDriver.WinChanged -= ChangeWin; -#endif - } - - _mainLoopDriver = null; - - WinConsole?.Cleanup (); - WinConsole = null; - - if (!RunningUnitTests && _isWindowsTerminal) - { - // Disable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); - } - } - - internal override MainLoop Init () - { - _mainLoopDriver = new WindowsMainLoop (this); - - if (!RunningUnitTests) - { - try - { - if (WinConsole is { }) - { - // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init. - // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED - Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos); - Cols = winSize.Width; - Rows = winSize.Height; - OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); - } - - WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); - - if (_isWindowsTerminal) - { - Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - } - } - catch (Win32Exception e) - { - // We are being run in an environment that does not support a console - // such as a unit test, or a pipe. - Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}"); - WinConsole = null; - } - } - - CurrentAttribute = new Attribute (Color.White, Color.Black); - - _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; - // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (Screen); - - _damageRegion = new WindowsConsole.SmallRect - { - Top = 0, - Left = 0, - Bottom = (short)Rows, - Right = (short)Cols - }; - - ClearContents (); - -#if HACK_CHECK_WINCHANGED - _mainLoopDriver.WinChanged = ChangeWin; -#endif - - WinConsole?.SetInitialCursorVisibility (); - return new MainLoop (_mainLoopDriver); - } - - internal void ProcessInput (WindowsConsole.InputRecord inputEvent) - { - switch (inputEvent.EventType) - { - case WindowsConsole.EventType.Key: - if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) - { - // Used to pass Unicode characters as if they were keystrokes. - // The VK_PACKET key is the low word of a 32-bit - // Virtual Key value used for non-keyboard input methods. - inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); - } - - WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); - - //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); - - KeyCode map = MapKey (keyInfo); - - if (map == KeyCode.Null) - { - break; - } - - if (inputEvent.KeyEvent.bKeyDown) - { - // Avoid sending repeat key down events - OnKeyDown (new Key (map)); - } - else - { - OnKeyUp (new Key (map)); - } - - break; - - case WindowsConsole.EventType.Mouse: - MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); - - if (me is null || me.Flags == MouseFlags.None) - { - break; - } - - OnMouseEvent (me); - - if (_processButtonClick) - { - OnMouseEvent (new () - { - Position = me.Position, - Flags = ProcessButtonClick (inputEvent.MouseEvent) - }); - } - - break; - - case WindowsConsole.EventType.Focus: - break; - -#if !HACK_CHECK_WINCHANGED - case WindowsConsole.EventType.WindowBufferSize: - - Cols = inputEvent.WindowBufferSizeEvent._size.X; - Rows = inputEvent.WindowBufferSizeEvent._size.Y; - - ResizeScreen (); - ClearContents (); - TerminalResized.Invoke (); - break; -#endif - } - } - -#if HACK_CHECK_WINCHANGED - private void ChangeWin (object s, SizeChangedEventArgs e) - { - if (e.Size is null) - { - return; - } - - int w = e.Size.Value.Width; - - if (w == Cols - 3 && e.Size.Value.Height < Rows) - { - w += 3; - } - - Left = 0; - Top = 0; - Cols = e.Size.Value.Width; - Rows = e.Size.Value.Height; - - if (!RunningUnitTests) - { - Size newSize = WinConsole.SetConsoleWindow ( - (short)Math.Max (w, 16), - (short)Math.Max (e.Size.Value.Height, 0)); - - Cols = newSize.Width; - Rows = newSize.Height; - } - - ResizeScreen (); - ClearContents (); - OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); - } -#endif - - private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) - { - ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo; - - switch (keyInfo.Key) - { - case ConsoleKey.D0: - case ConsoleKey.D1: - case ConsoleKey.D2: - case ConsoleKey.D3: - case ConsoleKey.D4: - case ConsoleKey.D5: - case ConsoleKey.D6: - case ConsoleKey.D7: - case ConsoleKey.D8: - case ConsoleKey.D9: - case ConsoleKey.NumPad0: - case ConsoleKey.NumPad1: - case ConsoleKey.NumPad2: - case ConsoleKey.NumPad3: - case ConsoleKey.NumPad4: - case ConsoleKey.NumPad5: - case ConsoleKey.NumPad6: - case ConsoleKey.NumPad7: - case ConsoleKey.NumPad8: - case ConsoleKey.NumPad9: - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - case ConsoleKey.Multiply: - case ConsoleKey.Add: - case ConsoleKey.Separator: - case ConsoleKey.Subtract: - case ConsoleKey.Decimal: - case ConsoleKey.Divide: - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - // These virtual key codes are mapped differently depending on the keyboard layout in use. - // We use the Win32 API to map them to the correct character. - uint mapResult = MapVKtoChar ((VK)keyInfo.Key); - - if (mapResult == 0) - { - // There is no mapping - this should not happen - Debug.Assert (mapResult != 0, $@"Unable to map the virtual key code {keyInfo.Key}."); - - return KeyCode.Null; - } - - // An un-shifted character value is in the low order word of the return value. - var mappedChar = (char)(mapResult & 0x0000FFFF); - - if (keyInfo.KeyChar == 0) - { - // If the keyChar is 0, keyInfo.Key value is not a printable character. - - // Dead keys (diacritics) are indicated by setting the top bit of the return value. - if ((mapResult & 0x80000000) != 0) - { - // Dead key (e.g. Oem2 '~'/'^' on POR keyboard) - // Option 1: Throw it out. - // - Apps will never see the dead keys - // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�'). - // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a'). - // - If user presses dead key again, the right thing happens (app will see `~~`) - // - This is what Notepad etc... appear to do - // Option 2: Expand the API to indicate the KeyCode is a dead key - // - Enables apps to do their own dead key processing - // - Adds complexity; no dev has asked for this (yet). - // We choose Option 1 for now. - return KeyCode.Null; - - // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported. - // Sadly, the charVal is just the deadkey and subsequent key events do not contain - // any info that the previous event was a deadkey. - // Note WT does not support Ctrl-Deadkey either. - } - - if (keyInfo.Modifiers != 0) - { - // These Oem keys have well-defined chars. We ensure the representative char is used. - // If we don't do this, then on some keyboard layouts the wrong char is - // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important - // for key persistence ("Ctrl++" vs. "Ctrl+="). - mappedChar = keyInfo.Key switch - { - ConsoleKey.OemPeriod => '.', - ConsoleKey.OemComma => ',', - ConsoleKey.OemPlus => '+', - ConsoleKey.OemMinus => '-', - _ => mappedChar - }; - } - - // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down - // we should keep it - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar); - } - - // KeyChar is printable - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar - return (KeyCode)keyInfo.KeyChar; - } - - if (keyInfo.Modifiers != ConsoleModifiers.Shift) - { - // If Shift wasn't down we don't need to do anything but return the mappedChar - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar); - } - - // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "�") - // and passing on Shift would be redundant. - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); - } - - // A..Z are special cased: - // - Alone, they represent lowercase a...z - // - With ShiftMask they are A..Z - // - If CapsLock is on the above is reversed. - // - If Alt and/or Ctrl are present, treat as upper case - if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) - { - if (keyInfo.KeyChar == 0) - { - // KeyChar is not printable - possibly an AltGr key? - // AltGr support - AltGr is equivalent to Ctrl+Alt - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); - } - } - - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); - } - - if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock) - { - // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask - if (char.IsUpper (keyInfo.KeyChar)) - { - return (KeyCode)(uint)keyInfo.Key | KeyCode.ShiftMask; - } - } - - // If KeyInfo.Key is A...Z and KeyInfo.KeyChar is not a...z then we have an accent. - // See https://github.com/gui-cs/Terminal.Gui/issues/3807#issuecomment-2455997595 - if (keyInfo.KeyChar <= (char)'z') - { - return (KeyCode)keyInfo.Key; - } - return (KeyCode)keyInfo.KeyChar; - } - - // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC - if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) - { - // If the key is JUST a modifier, return it as just that key - if (keyInfo.Key == (ConsoleKey)VK.SHIFT) - { // Shift 16 - return KeyCode.ShiftMask; - } - - if (keyInfo.Key == (ConsoleKey)VK.CONTROL) - { // Ctrl 17 - return KeyCode.CtrlMask; - } - - if (keyInfo.Key == (ConsoleKey)VK.MENU) - { // Alt 18 - return KeyCode.AltMask; - } - - if (keyInfo.KeyChar == 0) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - if (keyInfo.Key != ConsoleKey.None) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); - } - - // Handle control keys (e.g. CursorUp) - if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); - } - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent) - { - MouseFlags mouseFlag = 0; - - switch (_lastMouseButtonPressed) - { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Clicked; - - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Clicked; - - break; - - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button3Clicked; - - break; - } - - _point = new Point - { - X = mouseEvent.MousePosition.X, - Y = mouseEvent.MousePosition.Y - }; - _lastMouseButtonPressed = null; - _isButtonReleased = false; - _processButtonClick = false; - _point = null; - - return mouseFlag; - } - - private async Task ProcessButtonDoubleClickedAsync () - { - await Task.Delay (200); - _isButtonDoubleClicked = false; - _isOneFingerDoubleClicked = false; - - //buttonPressedCount = 0; - } - - private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag) - { - // When a user presses-and-holds, start generating pressed events every `startDelay` - // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms - const int startDelay = 500; - const int iterationsUntilFast = 4; - const int fastDelay = 50; - - int iterations = 0; - int delay = startDelay; - while (_isButtonPressed) - { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - View view = Application.WantContinuousButtonPressedView; - - if (view is null) - { - break; - } - - if (iterations++ >= iterationsUntilFast) - { - delay = fastDelay; - } - await Task.Delay (delay); - - var me = new MouseEventArgs - { - ScreenPosition = _pointMove, - Flags = mouseFlag - }; - - //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}"); - if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) - { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.Invoke (() => OnMouseEvent (me)); - } - } - } - - private void ResizeScreen () - { - _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; - // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (Screen); - - _damageRegion = new WindowsConsole.SmallRect - { - Top = 0, - Left = 0, - Bottom = (short)Rows, - Right = (short)Cols - }; - _dirtyLines = new bool [Rows]; - - WinConsole?.ForceRefreshCursorVisibility (); - } - - private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag) - { - if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed) - || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)) - { - mouseFlag |= MouseFlags.ButtonCtrl; - } - - if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) - { - mouseFlag |= MouseFlags.ButtonShift; - } - - if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) - || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) - { - mouseFlag |= MouseFlags.ButtonAlt; - } - - return mouseFlag; - } - - [CanBeNull] - private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) - { - var mouseFlag = MouseFlags.AllEvents; - - //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}"); - - if (_isButtonDoubleClicked || _isOneFingerDoubleClicked) - { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop.AddIdle ( - () => - { - Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); - - return false; - }); - } - - // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. - // This will tell when a mouse button is pressed. When the button is released this event will - // be fired with its bit set to 0. So when the button is up ButtonState will be 0. - // To map to the correct driver events we save the last pressed mouse button, so we can - // map to the correct clicked event. - if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0) - { - _lastMouseButtonPressed = null; - - //isButtonPressed = false; - _isButtonReleased = false; - } - - var p = new Point - { - X = mouseEvent.MousePosition.X, - Y = mouseEvent.MousePosition.Y - }; - - if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked) - || (_lastMouseButtonPressed == null - && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved) - && mouseEvent.ButtonState != 0 - && !_isButtonReleased - && !_isButtonDoubleClicked)) - { - switch (mouseEvent.ButtonState) - { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Pressed; - - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Pressed; - - break; - - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button3Pressed; - - break; - } - - if (_point is null) - { - _point = p; - } - - if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)) - { - _pointMove = p; - mouseFlag |= MouseFlags.ReportMousePosition; - _isButtonReleased = false; - _processButtonClick = false; - } - - _lastMouseButtonPressed = mouseEvent.ButtonState; - _isButtonPressed = true; - - if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) - { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop.AddIdle ( - () => - { - Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); - - return false; - }); - } - } - else if (_lastMouseButtonPressed != null - && mouseEvent.EventFlags == 0 - && !_isButtonReleased - && !_isButtonDoubleClicked - && !_isOneFingerDoubleClicked) - { - switch (_lastMouseButtonPressed) - { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1Released; - - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2Released; - - break; - - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button3Released; - - break; - } - - _isButtonPressed = false; - _isButtonReleased = true; - - if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y) - { - _processButtonClick = true; - } - else - { - _point = null; - } - _processButtonClick = true; - - } - else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved - && !_isOneFingerDoubleClicked - && _isButtonReleased - && p == _point) - { - mouseFlag = ProcessButtonClick (mouseEvent); - } - else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick)) - { - switch (mouseEvent.ButtonState) - { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1DoubleClicked; - - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2DoubleClicked; - - break; - - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button3DoubleClicked; - - break; - } - - _isButtonDoubleClicked = true; - } - else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked) - { - switch (mouseEvent.ButtonState) - { - case WindowsConsole.ButtonState.Button1Pressed: - mouseFlag = MouseFlags.Button1TripleClicked; - - break; - - case WindowsConsole.ButtonState.Button2Pressed: - mouseFlag = MouseFlags.Button2TripleClicked; - - break; - - case WindowsConsole.ButtonState.RightmostButtonPressed: - mouseFlag = MouseFlags.Button3TripleClicked; - - break; - } - - _isButtonDoubleClicked = false; - } - else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled) - { - switch ((int)mouseEvent.ButtonState) - { - case int v when v > 0: - mouseFlag = MouseFlags.WheeledUp; - - break; - - case int v when v < 0: - mouseFlag = MouseFlags.WheeledDown; - - break; - } - } - else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed) - { - switch ((int)mouseEvent.ButtonState) - { - case int v when v > 0: - mouseFlag = MouseFlags.WheeledLeft; - - break; - - case int v when v < 0: - mouseFlag = MouseFlags.WheeledRight; - - break; - } - } - else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled) - { - switch ((int)mouseEvent.ButtonState) - { - case int v when v < 0: - mouseFlag = MouseFlags.WheeledLeft; - - break; - - case int v when v > 0: - mouseFlag = MouseFlags.WheeledRight; - - break; - } - } - else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) - { - mouseFlag = MouseFlags.ReportMousePosition; - - if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y) - { - _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y); - } - } - else if (mouseEvent is { ButtonState: 0, EventFlags: 0 }) - { - // This happens on a double or triple click event. - mouseFlag = MouseFlags.None; - } - - mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag); - - //System.Diagnostics.Debug.WriteLine ( - // $"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}"); - - return new MouseEventArgs - { - Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y), - Flags = mouseFlag - }; - } -} - -/// -/// Mainloop intended to be used with the , and can -/// only be used on Windows. -/// -/// -/// This implementation is used for WindowsDriver. -/// -internal class WindowsMainLoop : IMainLoopDriver -{ - /// - /// Invoked when the window is changed. - /// - public EventHandler WinChanged; - - private readonly ConsoleDriver _consoleDriver; - private readonly ManualResetEventSlim _eventReady = new (false); - - // The records that we keep fetching - private readonly Queue _resultQueue = new (); - private readonly ManualResetEventSlim _waitForProbe = new (false); - private readonly WindowsConsole _winConsole; - private CancellationTokenSource _eventReadyTokenSource = new (); - private readonly CancellationTokenSource _inputHandlerTokenSource = new (); - private MainLoop _mainLoop; - - public WindowsMainLoop (ConsoleDriver consoleDriver = null) - { - _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); - _winConsole = ((WindowsDriver)consoleDriver).WinConsole; - } - - void IMainLoopDriver.Setup (MainLoop mainLoop) - { - _mainLoop = mainLoop; - Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token); -#if HACK_CHECK_WINCHANGED - Task.Run (CheckWinChange); -#endif - } - - void IMainLoopDriver.Wakeup () { _eventReady.Set (); } - - bool IMainLoopDriver.EventsPending () - { - _waitForProbe.Set (); -#if HACK_CHECK_WINCHANGED - _winChange.Set (); -#endif - if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout)) - { - return true; - } - - try - { - if (!_eventReadyTokenSource.IsCancellationRequested) - { - // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there - // are no timers, but there IS an idle handler waiting. - _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); - // - } - _eventReady.Reset (); - } - catch (OperationCanceledException) - { - _eventReady.Reset (); - return true; - } - finally - { - //_eventReady.Reset (); - } - - if (!_eventReadyTokenSource.IsCancellationRequested) - { -#if HACK_CHECK_WINCHANGED - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged; -#else - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); -#endif - } - - _eventReadyTokenSource.Dispose (); - _eventReadyTokenSource = new CancellationTokenSource (); - - return true; - } - - void IMainLoopDriver.Iteration () - { - while (_resultQueue.Count > 0) - { - WindowsConsole.InputRecord [] inputRecords = _resultQueue.Dequeue (); - - if (inputRecords is { Length: > 0 }) - { - ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]); - } - } -#if HACK_CHECK_WINCHANGED - if (_winChanged) - { - _winChanged = false; - WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize)); - } -#endif - } - - void IMainLoopDriver.TearDown () - { - _inputHandlerTokenSource?.Cancel (); - _inputHandlerTokenSource?.Dispose (); - - if (_winConsole is { }) - { - var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents (); - - if (numOfEvents > 0) - { - _winConsole.FlushConsoleInputBuffer (); - //Debug.WriteLine ($"Flushed {numOfEvents} events."); - } - } - - _waitForProbe?.Dispose (); - - _resultQueue?.Clear (); - - _eventReadyTokenSource?.Cancel (); - _eventReadyTokenSource?.Dispose (); - _eventReady?.Dispose (); - -#if HACK_CHECK_WINCHANGED - _winChange?.Dispose (); -#endif - - _mainLoop = null; - } - - private void WindowsInputHandler () - { - while (_mainLoop is { }) - { - try - { - if (!_inputHandlerTokenSource.IsCancellationRequested) - { - _waitForProbe.Wait (_inputHandlerTokenSource.Token); - } - } - catch (OperationCanceledException) - { - // Wakes the _waitForProbe if it's waiting - _waitForProbe.Set (); - return; - } - finally - { - // If IsCancellationRequested is true the code after - // the `finally` block will not be executed. - if (!_inputHandlerTokenSource.IsCancellationRequested) - { - _waitForProbe.Reset (); - } - } - - if (_resultQueue?.Count == 0) - { - var input = _winConsole.ReadConsoleInput (); - - //if (input [0].EventType != WindowsConsole.EventType.Focus) - { - _resultQueue.Enqueue (input); - } - } - - if (_resultQueue?.Count > 0) - { - _eventReady.Set (); - } - } - } - -#if HACK_CHECK_WINCHANGED - private readonly ManualResetEventSlim _winChange = new (false); - private bool _winChanged; - private Size _windowSize; - private void CheckWinChange () - { - while (_mainLoop is { }) - { - _winChange.Wait (); - _winChange.Reset (); - - // Check if the window size changed every half second. - // We do this to minimize the weird tearing seen on Windows when resizing the console - while (_mainLoop is { }) - { - Task.Delay (500).Wait (); - _windowSize = _winConsole.GetConsoleBufferWindow (out _); - - if (_windowSize != Size.Empty - && (_windowSize.Width != _consoleDriver.Cols - || _windowSize.Height != _consoleDriver.Rows)) - { - break; - } - } - - _winChanged = true; - _eventReady.Set (); - } - } -#endif -} - -internal class WindowsClipboard : ClipboardBase -{ - private const uint CF_UNICODE_TEXT = 13; - - public override bool IsSupported { get; } = CheckClipboardIsAvailable (); - - private static bool CheckClipboardIsAvailable () - { - // Attempt to open the clipboard - if (OpenClipboard (nint.Zero)) - { - // Clipboard is available - // Close the clipboard after use - CloseClipboard (); - - return true; - } - // Clipboard is not available - return false; - } - - protected override string GetClipboardDataImpl () - { - try - { - if (!OpenClipboard (nint.Zero)) - { - return string.Empty; - } - - nint handle = GetClipboardData (CF_UNICODE_TEXT); - - if (handle == nint.Zero) - { - return string.Empty; - } - - nint pointer = nint.Zero; - - try - { - pointer = GlobalLock (handle); - - if (pointer == nint.Zero) - { - return string.Empty; - } - - int size = GlobalSize (handle); - var buff = new byte [size]; - - Marshal.Copy (pointer, buff, 0, size); - - return Encoding.Unicode.GetString (buff).TrimEnd ('\0'); - } - finally - { - if (pointer != nint.Zero) - { - GlobalUnlock (handle); - } - } - } - finally - { - CloseClipboard (); - } - } - - protected override void SetClipboardDataImpl (string text) - { - OpenClipboard (); - - EmptyClipboard (); - nint hGlobal = default; - - try - { - int bytes = (text.Length + 1) * 2; - hGlobal = Marshal.AllocHGlobal (bytes); - - if (hGlobal == default (nint)) - { - ThrowWin32 (); - } - - nint target = GlobalLock (hGlobal); - - if (target == default (nint)) - { - ThrowWin32 (); - } - - try - { - Marshal.Copy (text.ToCharArray (), 0, target, text.Length); - } - finally - { - GlobalUnlock (target); - } - - if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint)) - { - ThrowWin32 (); - } - - hGlobal = default (nint); - } - finally - { - if (hGlobal != default (nint)) - { - Marshal.FreeHGlobal (hGlobal); - } - - CloseClipboard (); - } - } - - [DllImport ("user32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool CloseClipboard (); - - [DllImport ("user32.dll")] - private static extern bool EmptyClipboard (); - - [DllImport ("user32.dll", SetLastError = true)] - private static extern nint GetClipboardData (uint uFormat); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint GlobalLock (nint hMem); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern int GlobalSize (nint handle); - - [DllImport ("kernel32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool GlobalUnlock (nint hMem); - - [DllImport ("User32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool IsClipboardFormatAvailable (uint format); - - private void OpenClipboard () - { - var num = 10; - - while (true) - { - if (OpenClipboard (default (nint))) - { - break; - } - - if (--num == 0) - { - ThrowWin32 (); - } - - Thread.Sleep (100); - } - } - - [DllImport ("user32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool OpenClipboard (nint hWndNewOwner); - - [DllImport ("user32.dll", SetLastError = true)] - private static extern nint SetClipboardData (uint uFormat, nint data); - - private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); } -} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs new file mode 100644 index 0000000000..d38ffb4082 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs @@ -0,0 +1,179 @@ +#nullable enable +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Terminal.Gui; + +internal class WindowsClipboard : ClipboardBase +{ + private const uint CF_UNICODE_TEXT = 13; + + public override bool IsSupported { get; } = CheckClipboardIsAvailable (); + + private static bool CheckClipboardIsAvailable () + { + // Attempt to open the clipboard + if (OpenClipboard (nint.Zero)) + { + // Clipboard is available + // Close the clipboard after use + CloseClipboard (); + + return true; + } + // Clipboard is not available + return false; + } + + protected override string GetClipboardDataImpl () + { + try + { + if (!OpenClipboard (nint.Zero)) + { + return string.Empty; + } + + nint handle = GetClipboardData (CF_UNICODE_TEXT); + + if (handle == nint.Zero) + { + return string.Empty; + } + + nint pointer = nint.Zero; + + try + { + pointer = GlobalLock (handle); + + if (pointer == nint.Zero) + { + return string.Empty; + } + + int size = GlobalSize (handle); + var buff = new byte [size]; + + Marshal.Copy (pointer, buff, 0, size); + + return Encoding.Unicode.GetString (buff).TrimEnd ('\0'); + } + finally + { + if (pointer != nint.Zero) + { + GlobalUnlock (handle); + } + } + } + finally + { + CloseClipboard (); + } + } + + protected override void SetClipboardDataImpl (string text) + { + OpenClipboard (); + + EmptyClipboard (); + nint hGlobal = default; + + try + { + int bytes = (text.Length + 1) * 2; + hGlobal = Marshal.AllocHGlobal (bytes); + + if (hGlobal == default (nint)) + { + ThrowWin32 (); + } + + nint target = GlobalLock (hGlobal); + + if (target == default (nint)) + { + ThrowWin32 (); + } + + try + { + Marshal.Copy (text.ToCharArray (), 0, target, text.Length); + } + finally + { + GlobalUnlock (target); + } + + if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint)) + { + ThrowWin32 (); + } + + hGlobal = default (nint); + } + finally + { + if (hGlobal != default (nint)) + { + Marshal.FreeHGlobal (hGlobal); + } + + CloseClipboard (); + } + } + + [DllImport ("user32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool CloseClipboard (); + + [DllImport ("user32.dll")] + private static extern bool EmptyClipboard (); + + [DllImport ("user32.dll", SetLastError = true)] + private static extern nint GetClipboardData (uint uFormat); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint GlobalLock (nint hMem); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern int GlobalSize (nint handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool GlobalUnlock (nint hMem); + + [DllImport ("User32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool IsClipboardFormatAvailable (uint format); + + private void OpenClipboard () + { + var num = 10; + + while (true) + { + if (OpenClipboard (default (nint))) + { + break; + } + + if (--num == 0) + { + ThrowWin32 (); + } + + Thread.Sleep (100); + } + } + + [DllImport ("user32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool OpenClipboard (nint hWndNewOwner); + + [DllImport ("user32.dll", SetLastError = true)] + private static extern nint SetClipboardData (uint uFormat, nint data); + + private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); } +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs new file mode 100644 index 0000000000..2c6689737f --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -0,0 +1,1094 @@ +#nullable enable +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Terminal.Gui.ConsoleDrivers; + +namespace Terminal.Gui; + +internal class WindowsConsole +{ + private CancellationTokenSource? _inputReadyCancellationTokenSource; + private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); + internal WindowsMainLoop? _mainLoop; + + public const int STD_OUTPUT_HANDLE = -11; + public const int STD_INPUT_HANDLE = -10; + + private readonly nint _inputHandle; + private nint _outputHandle; + //private nint _screenBuffer; + private readonly uint _originalConsoleMode; + private CursorVisibility? _initialCursorVisibility; + private CursorVisibility? _currentCursorVisibility; + private CursorVisibility? _pendingCursorVisibility; + private readonly StringBuilder _stringBuilder = new (256 * 1024); + private string _lastWrite = string.Empty; + + public WindowsConsole () + { + _inputHandle = GetStdHandle (STD_INPUT_HANDLE); + _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); + _originalConsoleMode = ConsoleMode; + uint newConsoleMode = _originalConsoleMode; + newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); + newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; + ConsoleMode = newConsoleMode; + + _inputReadyCancellationTokenSource = new (); + Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); + } + + public InputRecord? DequeueInput () + { + while (_inputReadyCancellationTokenSource is { }) + { + try + { + return _inputQueue.Take (_inputReadyCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + return null; + } + } + + return null; + } + + public InputRecord? ReadConsoleInput () + { + const int BUFFER_SIZE = 1; + InputRecord inputRecord = default; + uint numberEventsRead = 0; + + while (!_inputReadyCancellationTokenSource!.IsCancellationRequested) + { + try + { + // Peek to check if there is any input available + if (PeekConsoleInput (_inputHandle, out _, BUFFER_SIZE, out uint eventsRead) && eventsRead > 0) + { + // Read the input since it is available + ReadConsoleInput ( + _inputHandle, + out inputRecord, + BUFFER_SIZE, + out numberEventsRead); + } + + if (numberEventsRead > 0) + { + return inputRecord; + } + + try + { + Task.Delay (100, _inputReadyCancellationTokenSource.Token).Wait (_inputReadyCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + return null; + } + } + catch (Exception ex) + { + if (ex is OperationCanceledException or ObjectDisposedException) + { + return null; + } + + throw; + } + } + + return null; + } + + private void ProcessInputQueue () + { + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + { + try + { + if (_inputQueue.Count == 0) + { + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + { + try + { + InputRecord? inpRec = ReadConsoleInput (); + + if (inpRec is { }) + { + _inputQueue.Add (inpRec.Value); + + break; + } + } + catch (OperationCanceledException) + { + return; + } + } + } + } + catch (OperationCanceledException) + { + return; + } + } + } + + private CharInfo []? _originalStdOutChars; + + public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) + { + //Debug.WriteLine ("WriteToConsole"); + + //if (_screenBuffer == nint.Zero) + //{ + // ReadFromConsoleOutput (size, bufferSize, ref window); + //} + + var result = false; + + if (force16Colors) + { + var i = 0; + CharInfo [] ci = new CharInfo [charInfoBuffer.Length]; + + foreach (ExtendedCharInfo info in charInfoBuffer) + { + ci [i++] = new CharInfo + { + Char = new CharUnion { UnicodeChar = info.Char }, + Attributes = + (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) + }; + } + + result = WriteConsoleOutput (_outputHandle, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window); + } + else + { + _stringBuilder.Clear (); + + _stringBuilder.Append (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; + + _inputReadyCancellationTokenSource?.Cancel (); + _inputReadyCancellationTokenSource?.Dispose (); + _inputReadyCancellationTokenSource = null; + } + + internal Size GetConsoleBufferWindow (out Point position) + { + if (_outputHandle == nint.Zero) + { + position = Point.Empty; + + return Size.Empty; + } + + var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); + csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi)) + { + //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + position = Point.Empty; + + return Size.Empty; + } + + Size sz = new ( + csbi.srWindow.Right - csbi.srWindow.Left + 1, + csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + position = new (csbi.srWindow.Left, csbi.srWindow.Top); + + return sz; + } + + internal Size GetConsoleOutputWindow (out Point position) + { + var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); + csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + if (!GetConsoleScreenBufferInfoEx (_outputHandle, ref csbi)) + { + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + + Size sz = new ( + csbi.srWindow.Right - csbi.srWindow.Left + 1, + csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + position = new (csbi.srWindow.Left, csbi.srWindow.Top); + + return sz; + } + + //internal Size SetConsoleWindow (short cols, short rows) + //{ + // var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); + // csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + // if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + + // Coord maxWinSize = GetLargestConsoleWindowSize (_screenBuffer); + // short newCols = Math.Min (cols, maxWinSize.X); + // short newRows = Math.Min (rows, maxWinSize.Y); + // csbi.dwSize = new Coord (newCols, Math.Max (newRows, (short)1)); + // csbi.srWindow = new SmallRect (0, 0, newCols, newRows); + // csbi.dwMaximumWindowSize = new Coord (newCols, newRows); + + // if (!SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + + // var winRect = new SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0)); + + // if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) + // { + // //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + // return new (cols, rows); + // } + + // SetConsoleOutputWindow (csbi); + + // return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1); + //} + + //private void SetConsoleOutputWindow (CONSOLE_SCREEN_BUFFER_INFOEX csbi) + //{ + // if (_screenBuffer != nint.Zero && !SetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + //} + + //internal Size SetConsoleOutputWindow (out Point position) + //{ + // if (_screenBuffer == nint.Zero) + // { + // position = Point.Empty; + + // return Size.Empty; + // } + + // var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); + // csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + // if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + + // Size sz = new ( + // csbi.srWindow.Right - csbi.srWindow.Left + 1, + // Math.Max (csbi.srWindow.Bottom - csbi.srWindow.Top + 1, 0)); + // position = new (csbi.srWindow.Left, csbi.srWindow.Top); + // SetConsoleOutputWindow (csbi); + // var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0)); + + // if (!SetConsoleScreenBufferInfoEx (_outputHandle, ref csbi)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + + // if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) + // { + // throw new Win32Exception (Marshal.GetLastWin32Error ()); + // } + + // return sz; + //} + + private uint ConsoleMode + { + get + { + GetConsoleMode (_inputHandle, out uint v); + + return v; + } + set => SetConsoleMode (_inputHandle, value); + } + + [Flags] + public enum ConsoleModes : uint + { + EnableProcessedInput = 1, + EnableMouseInput = 16, + EnableQuickEditMode = 64, + EnableExtendedFlags = 128 + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct KeyEventRecord + { + [FieldOffset (0)] + [MarshalAs (UnmanagedType.Bool)] + public bool bKeyDown; + + [FieldOffset (4)] + [MarshalAs (UnmanagedType.U2)] + public ushort wRepeatCount; + + [FieldOffset (6)] + [MarshalAs (UnmanagedType.U2)] + public ConsoleKeyMapping.VK wVirtualKeyCode; + + [FieldOffset (8)] + [MarshalAs (UnmanagedType.U2)] + public ushort wVirtualScanCode; + + [FieldOffset (10)] + public char UnicodeChar; + + [FieldOffset (12)] + [MarshalAs (UnmanagedType.U4)] + public ControlKeyState dwControlKeyState; + + public readonly override string ToString () + { + return + $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]"; + } + } + + [Flags] + public enum ButtonState + { + NoButtonPressed = 0, + Button1Pressed = 1, + Button2Pressed = 4, + Button3Pressed = 8, + Button4Pressed = 16, + RightmostButtonPressed = 2 + } + + [Flags] + public enum ControlKeyState + { + NoControlKeyPressed = 0, + RightAltPressed = 1, + LeftAltPressed = 2, + RightControlPressed = 4, + LeftControlPressed = 8, + ShiftPressed = 16, + NumlockOn = 32, + ScrolllockOn = 64, + CapslockOn = 128, + EnhancedKey = 256 + } + + [Flags] + public enum EventFlags + { + NoEvent = 0, + MouseMoved = 1, + DoubleClick = 2, + MouseWheeled = 4, + MouseHorizontalWheeled = 8 + } + + [StructLayout (LayoutKind.Explicit)] + public struct MouseEventRecord + { + [FieldOffset (0)] + public Coord MousePosition; + + [FieldOffset (4)] + public ButtonState ButtonState; + + [FieldOffset (8)] + public ControlKeyState ControlKeyState; + + [FieldOffset (12)] + public EventFlags EventFlags; + + public readonly override string ToString () { return $"[Mouse{MousePosition},{ButtonState},{ControlKeyState},{EventFlags}]"; } + } + + public struct WindowBufferSizeRecord + { + public Coord _size; + + public WindowBufferSizeRecord (short x, short y) { _size = new Coord (x, y); } + + public readonly override string ToString () { return $"[WindowBufferSize{_size}"; } + } + + [StructLayout (LayoutKind.Sequential)] + public struct MenuEventRecord + { + public uint dwCommandId; + } + + [StructLayout (LayoutKind.Sequential)] + public struct FocusEventRecord + { + public uint bSetFocus; + } + + public enum EventType : ushort + { + Focus = 0x10, + Key = 0x1, + Menu = 0x8, + Mouse = 2, + WindowBufferSize = 4 + } + + [StructLayout (LayoutKind.Explicit)] + public struct InputRecord + { + [FieldOffset (0)] + public EventType EventType; + + [FieldOffset (4)] + public KeyEventRecord KeyEvent; + + [FieldOffset (4)] + public MouseEventRecord MouseEvent; + + [FieldOffset (4)] + public WindowBufferSizeRecord WindowBufferSizeEvent; + + [FieldOffset (4)] + public MenuEventRecord MenuEvent; + + [FieldOffset (4)] + public FocusEventRecord FocusEvent; + + public readonly override string ToString () + { + return (EventType switch + { + EventType.Focus => FocusEvent.ToString (), + EventType.Key => KeyEvent.ToString (), + EventType.Menu => MenuEvent.ToString (), + EventType.Mouse => MouseEvent.ToString (), + EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), + _ => "Unknown event type: " + EventType + })!; + } + } + + [Flags] + private enum ShareMode : uint + { + FileShareRead = 1, + FileShareWrite = 2 + } + + [Flags] + private enum DesiredAccess : uint + { + GenericRead = 2147483648, + GenericWrite = 1073741824 + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleScreenBufferInfo + { + public Coord dwSize; + public Coord dwCursorPosition; + public ushort wAttributes; + public SmallRect srWindow; + public Coord dwMaximumWindowSize; + } + + [StructLayout (LayoutKind.Sequential)] + public struct Coord + { + public short X; + public short Y; + + public Coord (short x, short y) + { + X = x; + Y = y; + } + + public readonly override string ToString () { return $"({X},{Y})"; } + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharUnion + { + [FieldOffset (0)] + public char UnicodeChar; + + [FieldOffset (0)] + public byte AsciiChar; + } + + [StructLayout (LayoutKind.Explicit, CharSet = CharSet.Unicode)] + public struct CharInfo + { + [FieldOffset (0)] + public CharUnion Char; + + [FieldOffset (2)] + public ushort Attributes; + } + + public struct ExtendedCharInfo + { + public char Char { get; set; } + public Attribute Attribute { get; set; } + public bool Empty { get; set; } // TODO: Temp hack until virtual terminal sequences + + public ExtendedCharInfo (char character, Attribute attribute) + { + Char = character; + Attribute = attribute; + Empty = false; + } + } + + [StructLayout (LayoutKind.Sequential)] + public struct SmallRect + { + public short Left; + public short Top; + public short Right; + public short Bottom; + + public SmallRect (short left, short top, short right, short bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + + public static void MakeEmpty (ref SmallRect rect) { rect.Left = -1; } + + public static void Update (ref SmallRect rect, short col, short row) + { + if (rect.Left == -1) + { + rect.Left = rect.Right = col; + rect.Bottom = rect.Top = row; + + return; + } + + if (col >= rect.Left && col <= rect.Right && row >= rect.Top && row <= rect.Bottom) + { + return; + } + + if (col < rect.Left) + { + rect.Left = col; + } + + if (col > rect.Right) + { + rect.Right = col; + } + + if (row < rect.Top) + { + rect.Top = row; + } + + if (row > rect.Bottom) + { + rect.Bottom = row; + } + } + + public readonly override string ToString () { return $"Left={Left},Top={Top},Right={Right},Bottom={Bottom}"; } + } + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleKeyInfoEx + { + public ConsoleKeyInfo ConsoleKeyInfo; + public bool CapsLock; + public bool NumLock; + public bool ScrollLock; + + public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numlock, bool scrolllock) + { + ConsoleKeyInfo = consoleKeyInfo; + CapsLock = capslock; + NumLock = numlock; + ScrollLock = scrolllock; + } + + /// + /// Prints a ConsoleKeyInfoEx structure + /// + /// + /// + public readonly string ToString (ConsoleKeyInfoEx ex) + { + var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar); + var sb = new StringBuilder (); + sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})"); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); + sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) "); + sb.Append (ex.CapsLock ? "caps," : string.Empty); + sb.Append (ex.NumLock ? "num," : string.Empty); + sb.Append (ex.ScrollLock ? "scroll," : string.Empty); + string s = sb.ToString ().TrimEnd (',').TrimEnd (' '); + + return $"[ConsoleKeyInfoEx({s})]"; + } + } + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint GetStdHandle (int nStdHandle); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle (nint handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + public static extern bool PeekConsoleInput (nint hConsoleInput, out InputRecord lpBuffer, uint nLength, out uint lpNumberOfEventsRead); + + [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] + public static extern bool ReadConsoleInput ( + nint hConsoleInput, + out InputRecord lpBuffer, + uint nLength, + out uint lpNumberOfEventsRead + ); + + [DllImport ("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool ReadConsoleOutput ( + nint hConsoleOutput, + [Out] CharInfo [] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpReadRegion + ); + + // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput + [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool WriteConsoleOutput ( + nint hConsoleOutput, + CharInfo [] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpWriteRegion + ); + + [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool WriteConsole ( + nint hConsoleOutput, + string lpbufer, + uint NumberOfCharsToWriten, + out uint lpNumberOfCharsWritten, + nint lpReserved + ); + + [DllImport ("kernel32.dll", SetLastError = true)] + static extern bool FlushFileBuffers (nint hFile); + + [DllImport ("kernel32.dll")] + private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition); + + [StructLayout (LayoutKind.Sequential)] + public struct ConsoleCursorInfo + { + /// + /// The percentage of the character cell that is filled by the cursor.This value is between 1 and 100. + /// The cursor appearance varies, ranging from completely filling the cell to showing up as a horizontal + /// line at the bottom of the cell. + /// + public uint dwSize; + public bool bVisible; + } + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref ConsoleCursorInfo lpConsoleCursorInfo); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool GetConsoleCursorInfo (nint hConsoleOutput, out ConsoleCursorInfo lpConsoleCursorInfo); + + [DllImport ("kernel32.dll")] + private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); + + [DllImport ("kernel32.dll")] + private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint CreateConsoleScreenBuffer ( + DesiredAccess dwDesiredAccess, + ShareMode dwShareMode, + nint secutiryAttributes, + uint flags, + nint screenBufferData + ); + + internal static nint INVALID_HANDLE_VALUE = new (-1); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleActiveScreenBuffer (nint handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents); + + internal uint GetNumberOfConsoleInputEvents () + { + if (!GetNumberOfConsoleInputEvents (_inputHandle, out uint numOfEvents)) + { + Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}"); + + return 0; + } + + return numOfEvents; + } + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool FlushConsoleInputBuffer (nint handle); + + internal void FlushConsoleInputBuffer () + { + if (!FlushConsoleInputBuffer (_inputHandle)) + { + Console.WriteLine ($"Error: {Marshal.GetLastWin32Error ()}"); + } + } + +#if false // Not needed on the constructor. Perhaps could be used on resizing. To study. + [DllImport ("kernel32.dll", ExactSpelling = true)] + static extern IntPtr GetConsoleWindow (); + + [DllImport ("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern bool ShowWindow (IntPtr hWnd, int nCmdShow); + + public const int HIDE = 0; + public const int MAXIMIZE = 3; + public const int MINIMIZE = 6; + public const int RESTORE = 9; + + internal void ShowWindow (int state) + { + IntPtr thisConsole = GetConsoleWindow (); + ShowWindow (thisConsole, state); + } +#endif + + // See: https://github.com/gui-cs/Terminal.Gui/issues/357 + + [StructLayout (LayoutKind.Sequential)] + public struct CONSOLE_SCREEN_BUFFER_INFOEX + { + public uint cbSize; + public Coord dwSize; + public Coord dwCursorPosition; + public ushort wAttributes; + public SmallRect srWindow; + public Coord dwMaximumWindowSize; + public ushort wPopupAttributes; + public bool bFullscreenSupported; + + [MarshalAs (UnmanagedType.ByValArray, SizeConst = 16)] + public COLORREF [] ColorTable; + } + + [StructLayout (LayoutKind.Explicit, Size = 4)] + public struct COLORREF + { + public COLORREF (byte r, byte g, byte b) + { + Value = 0; + R = r; + G = g; + B = b; + } + + public COLORREF (uint value) + { + R = 0; + G = 0; + B = 0; + Value = value & 0x00FFFFFF; + } + + [FieldOffset (0)] + public byte R; + + [FieldOffset (1)] + public byte G; + + [FieldOffset (2)] + public byte B; + + [FieldOffset (0)] + public uint Value; + } + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleWindowInfo ( + nint hConsoleOutput, + bool bAbsolute, + [In] ref SmallRect lpConsoleWindow + ); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern Coord GetLargestConsoleWindowSize ( + nint hConsoleOutput + ); +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs new file mode 100644 index 0000000000..251ad4dad9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -0,0 +1,1190 @@ +#nullable enable +// +// WindowsDriver.cs: Windows specific driver +// + +// HACK: +// WindowsConsole/Terminal has two issues: +// 1) Tearing can occur when the console is resized. +// 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct. +// +// If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events +// and instead check the console size every 500ms in a thread in WidowsMainLoop. +// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using +// the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is +// still incorrect so we still need this hack. + +//#define HACK_CHECK_WINCHANGED + +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; + +namespace Terminal.Gui; + +internal class WindowsDriver : ConsoleDriver +{ + private readonly bool _isWindowsTerminal; + + private WindowsConsole.SmallRect _damageRegion; + private bool _isButtonDoubleClicked; + private bool _isButtonPressed; + private bool _isButtonReleased; + private bool _isOneFingerDoubleClicked; + + private WindowsConsole.ButtonState? _lastMouseButtonPressed; + private WindowsMainLoop? _mainLoopDriver; + private WindowsConsole.ExtendedCharInfo [] _outputBuffer; + private Point? _point; + private Point _pointMove; + private bool _processButtonClick; + + public WindowsDriver () + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + WinConsole = new (); + + // otherwise we're probably running in unit tests + Clipboard = new WindowsClipboard (); + } + else + { + Clipboard = new FakeDriver.FakeClipboard (); + } + + // TODO: if some other Windows-based terminal supports true color, update this logic to not + // force 16color mode (.e.g ConEmu which really doesn't work well at all). + _isWindowsTerminal = _isWindowsTerminal = + Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null; + + if (!_isWindowsTerminal) + { + Force16Colors = true; + } + } + + public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal); + + public WindowsConsole? WinConsole { get; private set; } + + public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) + { + if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet) + { + return keyEvent; + } + + var mod = new ConsoleModifiers (); + + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) + { + mod |= ConsoleModifiers.Shift; + } + + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) + || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) + { + mod |= ConsoleModifiers.Alt; + } + + if (keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed) + || keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) + { + mod |= ConsoleModifiers.Control; + } + + var cKeyInfo = new ConsoleKeyInfo ( + keyEvent.UnicodeChar, + (ConsoleKey)keyEvent.wVirtualKeyCode, + mod.HasFlag (ConsoleModifiers.Shift), + mod.HasFlag (ConsoleModifiers.Alt), + mod.HasFlag (ConsoleModifiers.Control)); + cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (cKeyInfo); + uint scanCode = GetScanCodeFromConsoleKeyInfo (cKeyInfo); + + return new WindowsConsole.KeyEventRecord + { + UnicodeChar = cKeyInfo.KeyChar, + bKeyDown = keyEvent.bKeyDown, + dwControlKeyState = keyEvent.dwControlKeyState, + wRepeatCount = keyEvent.wRepeatCount, + wVirtualKeyCode = (VK)cKeyInfo.Key, + wVirtualScanCode = (ushort)scanCode + }; + } + + public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; } + + public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) + { + var input = new WindowsConsole.InputRecord + { + EventType = WindowsConsole.EventType.Key + }; + + var keyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = true + }; + var controlKey = new WindowsConsole.ControlKeyState (); + + if (shift) + { + controlKey |= WindowsConsole.ControlKeyState.ShiftPressed; + keyEvent.UnicodeChar = '\0'; + keyEvent.wVirtualKeyCode = VK.SHIFT; + } + + if (alt) + { + controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed; + controlKey |= WindowsConsole.ControlKeyState.RightAltPressed; + keyEvent.UnicodeChar = '\0'; + keyEvent.wVirtualKeyCode = VK.MENU; + } + + if (control) + { + controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed; + controlKey |= WindowsConsole.ControlKeyState.RightControlPressed; + keyEvent.UnicodeChar = '\0'; + keyEvent.wVirtualKeyCode = VK.CONTROL; + } + + keyEvent.dwControlKeyState = controlKey; + + input.KeyEvent = keyEvent; + + if (shift || alt || control) + { + ProcessInput (input); + } + + keyEvent.UnicodeChar = keyChar; + + //if ((uint)key < 255) { + // keyEvent.wVirtualKeyCode = (ushort)key; + //} else { + // keyEvent.wVirtualKeyCode = '\0'; + //} + keyEvent.wVirtualKeyCode = (VK)key; + + input.KeyEvent = keyEvent; + + try + { + ProcessInput (input); + } + catch (OverflowException) + { } + finally + { + keyEvent.bKeyDown = false; + input.KeyEvent = keyEvent; + ProcessInput (input); + } + } + + public override void WriteRaw (string ansi) { WinConsole?.WriteANSI (ansi); } + + #region Not Implemented + + public override void Suspend () { throw new NotImplementedException (); } + + #endregion + + public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) + { + WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState; + + bool shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; + bool alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; + bool control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; + bool capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0; + bool numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0; + bool scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0; + + var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + + return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock); + } + + #region Cursor Handling + + private CursorVisibility? _cachedCursorVisibility; + + public override void UpdateCursor () + { + if (RunningUnitTests) + { + return; + } + + if (Col < 0 || Row < 0 || Col >= Cols || Row >= Rows) + { + GetCursorVisibility (out CursorVisibility cursorVisibility); + _cachedCursorVisibility = cursorVisibility; + SetCursorVisibility (CursorVisibility.Invisible); + + return; + } + + var position = new WindowsConsole.Coord + { + X = (short)Col, + Y = (short)Row + }; + + if (Force16Colors) + { + WinConsole?.SetCursorPosition (position); + } + else + { + var sb = new StringBuilder (); + sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1)); + WinConsole?.WriteANSI (sb.ToString ()); + } + + if (_cachedCursorVisibility is { }) + { + SetCursorVisibility (_cachedCursorVisibility.Value); + } + //EnsureCursorVisibility (); + } + + /// + public override bool GetCursorVisibility (out CursorVisibility visibility) + { + if (WinConsole is { }) + { + return WinConsole.GetCursorVisibility (out visibility); + } + + visibility = _cachedCursorVisibility ?? CursorVisibility.Default; + + return true; + } + + /// + public override bool SetCursorVisibility (CursorVisibility visibility) + { + _cachedCursorVisibility = visibility; + + if (Force16Colors) + { + return WinConsole is null || WinConsole.SetCursorVisibility (visibility); + } + else + { + var sb = new StringBuilder (); + sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + return WinConsole?.WriteANSI (sb.ToString ()) ?? false; + } + } + + /// + public override bool EnsureCursorVisibility () + { + if (Force16Colors) + { + return WinConsole is null || WinConsole.EnsureCursorVisibility (); + } + else + { + var sb = new StringBuilder (); + sb.Append (_cachedCursorVisibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + return WinConsole?.WriteANSI (sb.ToString ()) ?? false; + } + + //if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) + //{ + // GetCursorVisibility (out CursorVisibility cursorVisibility); + // _cachedCursorVisibility = cursorVisibility; + // SetCursorVisibility (CursorVisibility.Invisible); + + // return false; + //} + + //SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default); + + //return _cachedCursorVisibility == CursorVisibility.Default; + } + + #endregion Cursor Handling + + public override bool UpdateScreen () + { + bool updated = false; + Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows); + + if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows)) + { + return updated; + } + + var bufferCoords = new WindowsConsole.Coord + { + X = (short)Cols, //Clip.Width, + Y = (short)Rows, //Clip.Height + }; + + for (var row = 0; row < Rows; row++) + { + if (!_dirtyLines! [row]) + { + continue; + } + + _dirtyLines [row] = false; + updated = true; + + for (var col = 0; col < Cols; col++) + { + int position = row * Cols + col; + _outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault (); + + if (Contents [row, col].IsDirty == false) + { + _outputBuffer [position].Empty = true; + _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + + continue; + } + + _outputBuffer [position].Empty = false; + + if (Contents [row, col].Rune.IsBmp) + { + _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value; + } + else + { + //_outputBuffer [position].Empty = true; + _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + + if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols) + { + // TODO: This is a hack to deal with non-BMP and wide characters. + col++; + position = row * Cols + col; + _outputBuffer [position].Empty = false; + _outputBuffer [position].Char = ' '; + } + } + } + } + + _damageRegion = new WindowsConsole.SmallRect + { + Top = 0, + Left = 0, + Bottom = (short)Rows, + Right = (short)Cols + }; + + if (!RunningUnitTests + && WinConsole != null + && !WinConsole.WriteToConsole (new (Cols, Rows), _outputBuffer, bufferCoords, _damageRegion, Force16Colors)) + { + int err = Marshal.GetLastWin32Error (); + + if (err != 0) + { + throw new Win32Exception (err); + } + } + + WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); + + return updated; + } + + public override void End () + { + if (_mainLoopDriver is { }) + { +#if HACK_CHECK_WINCHANGED + + _mainLoopDriver.WinChanged -= ChangeWin; +#endif + } + + _mainLoopDriver = null; + + WinConsole?.Cleanup (); + WinConsole = null; + + if (!RunningUnitTests && _isWindowsTerminal) + { + // Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + } + } + + public override MainLoop Init () + { + _mainLoopDriver = new WindowsMainLoop (this); + + if (!RunningUnitTests) + { + try + { + if (WinConsole is { }) + { + // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init. + // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED + Size winSize = WinConsole.GetConsoleOutputWindow (out Point _); + Cols = winSize.Width; + Rows = winSize.Height; + OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); + } + + WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); + + if (_isWindowsTerminal) + { + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + } + } + catch (Win32Exception e) + { + // We are being run in an environment that does not support a console + // such as a unit test, or a pipe. + Debug.WriteLine ($"Likely running unit tests. Setting WinConsole to null so we can test it elsewhere. Exception: {e}"); + WinConsole = null; + } + } + + CurrentAttribute = new Attribute (Color.White, Color.Black); + + _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; + // CONCURRENCY: Unsynchronized access to Clip is not safe. + Clip = new (Screen); + + _damageRegion = new WindowsConsole.SmallRect + { + Top = 0, + Left = 0, + Bottom = (short)Rows, + Right = (short)Cols + }; + + ClearContents (); + +#if HACK_CHECK_WINCHANGED + _mainLoopDriver.WinChanged = ChangeWin; +#endif + + if (!RunningUnitTests) + { + WinConsole?.SetInitialCursorVisibility (); + } + + return new MainLoop (_mainLoopDriver); + } + + internal void ProcessInput (WindowsConsole.InputRecord inputEvent) + { + switch (inputEvent.EventType) + { + case WindowsConsole.EventType.Key: + if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) + { + // Used to pass Unicode characters as if they were keystrokes. + // The VK_PACKET key is the low word of a 32-bit + // Virtual Key value used for non-keyboard input methods. + inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); + } + + WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); + + //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); + + KeyCode map = MapKey (keyInfo); + + if (map == KeyCode.Null) + { + break; + } + + if (inputEvent.KeyEvent.bKeyDown) + { + // Avoid sending repeat key down events + OnKeyDown (new Key (map)); + } + else + { + OnKeyUp (new Key (map)); + } + + break; + + case WindowsConsole.EventType.Mouse: + MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); + + if (me.Flags == MouseFlags.None) + { + break; + } + + OnMouseEvent (me); + + if (_processButtonClick) + { + OnMouseEvent (new () + { + Position = me.Position, + Flags = ProcessButtonClick (inputEvent.MouseEvent) + }); + } + + break; + + case WindowsConsole.EventType.Focus: + break; + +#if !HACK_CHECK_WINCHANGED + case WindowsConsole.EventType.WindowBufferSize: + + Cols = inputEvent.WindowBufferSizeEvent._size.X; + Rows = inputEvent.WindowBufferSizeEvent._size.Y; + Application.Screen = new (0, 0, Cols, Rows); + + ResizeScreen (); + ClearContents (); + Application.Top?.SetNeedsLayout (); + Application.LayoutAndDraw (); + + break; +#endif + } + } + +#if HACK_CHECK_WINCHANGED + private void ChangeWin (object s, SizeChangedEventArgs e) + { + if (e.Size is null) + { + return; + } + + int w = e.Size.Value.Width; + + if (w == Cols - 3 && e.Size.Value.Height < Rows) + { + w += 3; + } + + Left = 0; + Top = 0; + Cols = e.Size.Value.Width; + Rows = e.Size.Value.Height; + + if (!RunningUnitTests) + { + Size newSize = WinConsole.SetConsoleWindow ( + (short)Math.Max (w, 16), + (short)Math.Max (e.Size.Value.Height, 0)); + + Cols = newSize.Width; + Rows = newSize.Height; + } + + ResizeScreen (); + ClearContents (); + OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); + } +#endif + + private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) + { + ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo; + + switch (keyInfo.Key) + { + case ConsoleKey.D0: + case ConsoleKey.D1: + case ConsoleKey.D2: + case ConsoleKey.D3: + case ConsoleKey.D4: + case ConsoleKey.D5: + case ConsoleKey.D6: + case ConsoleKey.D7: + case ConsoleKey.D8: + case ConsoleKey.D9: + case ConsoleKey.NumPad0: + case ConsoleKey.NumPad1: + case ConsoleKey.NumPad2: + case ConsoleKey.NumPad3: + case ConsoleKey.NumPad4: + case ConsoleKey.NumPad5: + case ConsoleKey.NumPad6: + case ConsoleKey.NumPad7: + case ConsoleKey.NumPad8: + case ConsoleKey.NumPad9: + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + case ConsoleKey.Multiply: + case ConsoleKey.Add: + case ConsoleKey.Separator: + case ConsoleKey.Subtract: + case ConsoleKey.Decimal: + case ConsoleKey.Divide: + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + // These virtual key codes are mapped differently depending on the keyboard layout in use. + // We use the Win32 API to map them to the correct character. + uint mapResult = MapVKtoChar ((VK)keyInfo.Key); + + if (mapResult == 0) + { + // There is no mapping - this should not happen + Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}."); + + return KeyCode.Null; + } + + // An un-shifted character value is in the low order word of the return value. + var mappedChar = (char)(mapResult & 0x0000FFFF); + + if (keyInfo.KeyChar == 0) + { + // If the keyChar is 0, keyInfo.Key value is not a printable character. + + // Dead keys (diacritics) are indicated by setting the top bit of the return value. + if ((mapResult & 0x80000000) != 0) + { + // Dead key (e.g. Oem2 '~'/'^' on POR keyboard) + // Option 1: Throw it out. + // - Apps will never see the dead keys + // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�'). + // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a'). + // - If user presses dead key again, the right thing happens (app will see `~~`) + // - This is what Notepad etc... appear to do + // Option 2: Expand the API to indicate the KeyCode is a dead key + // - Enables apps to do their own dead key processing + // - Adds complexity; no dev has asked for this (yet). + // We choose Option 1 for now. + return KeyCode.Null; + + // Note: Ctrl-Deadkey (like Oem3 '`'/'~` on ENG) can't be supported. + // Sadly, the charVal is just the deadkey and subsequent key events do not contain + // any info that the previous event was a deadkey. + // Note WT does not support Ctrl-Deadkey either. + } + + if (keyInfo.Modifiers != 0) + { + // These Oem keys have well-defined chars. We ensure the representative char is used. + // If we don't do this, then on some keyboard layouts the wrong char is + // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important + // for key persistence ("Ctrl++" vs. "Ctrl+="). + mappedChar = keyInfo.Key switch + { + ConsoleKey.OemPeriod => '.', + ConsoleKey.OemComma => ',', + ConsoleKey.OemPlus => '+', + ConsoleKey.OemMinus => '-', + _ => mappedChar + }; + } + + // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down + // we should keep it + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar); + } + + // KeyChar is printable + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + // AltGr support - AltGr is equivalent to Ctrl+Alt - the correct char is in KeyChar + return (KeyCode)keyInfo.KeyChar; + } + + if (keyInfo.Modifiers != ConsoleModifiers.Shift) + { + // If Shift wasn't down we don't need to do anything but return the mappedChar + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)mappedChar); + } + + // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "�") + // and passing on Shift would be redundant. + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); + } + + // A..Z are special cased: + // - Alone, they represent lowercase a...z + // - With ShiftMask they are A..Z + // - If CapsLock is on the above is reversed. + // - If Alt and/or Ctrl are present, treat as upper case + if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) + { + if (keyInfo.KeyChar == 0) + { + // KeyChar is not printable - possibly an AltGr key? + // AltGr support - AltGr is equivalent to Ctrl+Alt + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) && keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); + } + } + + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)(uint)keyInfo.Key); + } + + if ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ keyInfoEx.CapsLock) + { + // If (ShiftMask is on and CapsLock is off) or (ShiftMask is off and CapsLock is on) add the ShiftMask + if (char.IsUpper (keyInfo.KeyChar)) + { + if (keyInfo.KeyChar <= 'Z') + { + return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; + } + + // Always return the KeyChar because it may be an Á, À with Oem1, etc + return (KeyCode)keyInfo.KeyChar; + } + } + + if (keyInfo.KeyChar <= 'z') + { + return (KeyCode)keyInfo.Key; + } + + // Always return the KeyChar because it may be an á, à with Oem1, etc + return (KeyCode)keyInfo.KeyChar; + } + + // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC + if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) + { + // If the key is JUST a modifier, return it as just that key + if (keyInfo.Key == (ConsoleKey)VK.SHIFT) + { // Shift 16 + return KeyCode.ShiftMask; + } + + if (keyInfo.Key == (ConsoleKey)VK.CONTROL) + { // Ctrl 17 + return KeyCode.CtrlMask; + } + + if (keyInfo.Key == (ConsoleKey)VK.MENU) + { // Alt 18 + return KeyCode.AltMask; + } + + if (keyInfo.KeyChar == 0) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + + if (keyInfo.Key != ConsoleKey.None) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); + } + + // Handle control keys (e.g. CursorUp) + if (Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); + } + + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + + private MouseFlags ProcessButtonClick (WindowsConsole.MouseEventRecord mouseEvent) + { + MouseFlags mouseFlag = 0; + + switch (_lastMouseButtonPressed) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Clicked; + + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Clicked; + + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Clicked; + + break; + } + + _point = new Point + { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + _lastMouseButtonPressed = null; + _isButtonReleased = false; + _processButtonClick = false; + _point = null; + + return mouseFlag; + } + + private async Task ProcessButtonDoubleClickedAsync () + { + await Task.Delay (200); + _isButtonDoubleClicked = false; + _isOneFingerDoubleClicked = false; + + //buttonPressedCount = 0; + } + + private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag) + { + // When a user presses-and-holds, start generating pressed events every `startDelay` + // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms + const int START_DELAY = 500; + const int ITERATIONS_UNTIL_FAST = 4; + const int FAST_DELAY = 50; + + int iterations = 0; + int delay = START_DELAY; + while (_isButtonPressed) + { + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + View? view = Application.WantContinuousButtonPressedView; + + if (view is null) + { + break; + } + + if (iterations++ >= ITERATIONS_UNTIL_FAST) + { + delay = FAST_DELAY; + } + await Task.Delay (delay); + + var me = new MouseEventArgs + { + ScreenPosition = _pointMove, + Flags = mouseFlag + }; + + //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}"); + if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) + { + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + Application.Invoke (() => OnMouseEvent (me)); + } + } + } + + private void ResizeScreen () + { + _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; + // CONCURRENCY: Unsynchronized access to Clip is not safe. + Clip = new (Screen); + + _damageRegion = new WindowsConsole.SmallRect + { + Top = 0, + Left = 0, + Bottom = (short)Rows, + Right = (short)Cols + }; + _dirtyLines = new bool [Rows]; + + WinConsole?.ForceRefreshCursorVisibility (); + } + + private static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEvent, MouseFlags mouseFlag) + { + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed) + || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftControlPressed)) + { + mouseFlag |= MouseFlags.ButtonCtrl; + } + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ShiftPressed)) + { + mouseFlag |= MouseFlags.ButtonShift; + } + + if (mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightAltPressed) + || mouseEvent.ControlKeyState.HasFlag (WindowsConsole.ControlKeyState.LeftAltPressed)) + { + mouseFlag |= MouseFlags.ButtonAlt; + } + + return mouseFlag; + } + + [CanBeNull] + private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent) + { + var mouseFlag = MouseFlags.AllEvents; + + //Debug.WriteLine ($"ToDriverMouse: {mouseEvent}"); + + if (_isButtonDoubleClicked || _isOneFingerDoubleClicked) + { + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + Application.MainLoop!.AddIdle ( + () => + { + Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); + + return false; + }); + } + + // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. + // This will tell when a mouse button is pressed. When the button is released this event will + // be fired with its bit set to 0. So when the button is up ButtonState will be 0. + // To map to the correct driver events we save the last pressed mouse button, so we can + // map to the correct clicked event. + if ((_lastMouseButtonPressed is { } || _isButtonReleased) && mouseEvent.ButtonState != 0) + { + _lastMouseButtonPressed = null; + + //isButtonPressed = false; + _isButtonReleased = false; + } + + var p = new Point + { + X = mouseEvent.MousePosition.X, + Y = mouseEvent.MousePosition.Y + }; + + if ((mouseEvent.ButtonState != 0 && mouseEvent.EventFlags == 0 && _lastMouseButtonPressed is null && !_isButtonDoubleClicked) + || (_lastMouseButtonPressed == null + && mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved) + && mouseEvent.ButtonState != 0 + && !_isButtonReleased + && !_isButtonDoubleClicked)) + { + switch (mouseEvent.ButtonState) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Pressed; + + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Pressed; + + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Pressed; + + break; + } + + if (_point is null) + { + _point = p; + } + + if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.MouseMoved)) + { + _pointMove = p; + mouseFlag |= MouseFlags.ReportMousePosition; + _isButtonReleased = false; + _processButtonClick = false; + } + + _lastMouseButtonPressed = mouseEvent.ButtonState; + _isButtonPressed = true; + + if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) + { + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + Application.MainLoop!.AddIdle ( + () => + { + Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); + + return false; + }); + } + } + else if (_lastMouseButtonPressed != null + && mouseEvent.EventFlags == 0 + && !_isButtonReleased + && !_isButtonDoubleClicked + && !_isOneFingerDoubleClicked) + { + switch (_lastMouseButtonPressed) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1Released; + + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2Released; + + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3Released; + + break; + } + + _isButtonPressed = false; + _isButtonReleased = true; + + if (_point is { } && ((Point)_point).X == mouseEvent.MousePosition.X && ((Point)_point).Y == mouseEvent.MousePosition.Y) + { + _processButtonClick = true; + } + else + { + _point = null; + } + _processButtonClick = true; + + } + else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved + && !_isOneFingerDoubleClicked + && _isButtonReleased + && p == _point) + { + mouseFlag = ProcessButtonClick (mouseEvent); + } + else if (mouseEvent.EventFlags.HasFlag (WindowsConsole.EventFlags.DoubleClick)) + { + switch (mouseEvent.ButtonState) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1DoubleClicked; + + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2DoubleClicked; + + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3DoubleClicked; + + break; + } + + _isButtonDoubleClicked = true; + } + else if (mouseEvent.EventFlags == 0 && mouseEvent.ButtonState != 0 && _isButtonDoubleClicked) + { + switch (mouseEvent.ButtonState) + { + case WindowsConsole.ButtonState.Button1Pressed: + mouseFlag = MouseFlags.Button1TripleClicked; + + break; + + case WindowsConsole.ButtonState.Button2Pressed: + mouseFlag = MouseFlags.Button2TripleClicked; + + break; + + case WindowsConsole.ButtonState.RightmostButtonPressed: + mouseFlag = MouseFlags.Button3TripleClicked; + + break; + } + + _isButtonDoubleClicked = false; + } + else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled) + { + switch ((int)mouseEvent.ButtonState) + { + case int v when v > 0: + mouseFlag = MouseFlags.WheeledUp; + + break; + + case int v when v < 0: + mouseFlag = MouseFlags.WheeledDown; + + break; + } + } + else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseWheeled && mouseEvent.ControlKeyState == WindowsConsole.ControlKeyState.ShiftPressed) + { + switch ((int)mouseEvent.ButtonState) + { + case int v when v > 0: + mouseFlag = MouseFlags.WheeledLeft; + + break; + + case int v when v < 0: + mouseFlag = MouseFlags.WheeledRight; + + break; + } + } + else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseHorizontalWheeled) + { + switch ((int)mouseEvent.ButtonState) + { + case int v when v < 0: + mouseFlag = MouseFlags.WheeledLeft; + + break; + + case int v when v > 0: + mouseFlag = MouseFlags.WheeledRight; + + break; + } + } + else if (mouseEvent.EventFlags == WindowsConsole.EventFlags.MouseMoved) + { + mouseFlag = MouseFlags.ReportMousePosition; + + if (mouseEvent.MousePosition.X != _pointMove.X || mouseEvent.MousePosition.Y != _pointMove.Y) + { + _pointMove = new Point (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y); + } + } + else if (mouseEvent is { ButtonState: 0, EventFlags: 0 }) + { + // This happens on a double or triple click event. + mouseFlag = MouseFlags.None; + } + + mouseFlag = SetControlKeyStates (mouseEvent, mouseFlag); + + //System.Diagnostics.Debug.WriteLine ( + // $"point.X:{(point is { } ? ((Point)point).X : -1)};point.Y:{(point is { } ? ((Point)point).Y : -1)}"); + + return new MouseEventArgs + { + Position = new (mouseEvent.MousePosition.X, mouseEvent.MousePosition.Y), + Flags = mouseFlag + }; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs new file mode 100644 index 0000000000..f1ffafdca0 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs @@ -0,0 +1,240 @@ +#nullable enable + +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +/// +/// Mainloop intended to be used with the , and can +/// only be used on Windows. +/// +/// +/// This implementation is used for WindowsDriver. +/// +internal class WindowsMainLoop : IMainLoopDriver +{ + /// + /// Invoked when the window is changed. + /// + public EventHandler? WinChanged; + + private readonly IConsoleDriver _consoleDriver; + private readonly ManualResetEventSlim _eventReady = new (false); + + // The records that we keep fetching + private readonly ConcurrentQueue _resultQueue = new (); + private readonly ManualResetEventSlim _waitForProbe = new (false); + private readonly WindowsConsole? _winConsole; + private CancellationTokenSource _eventReadyTokenSource = new (); + private readonly CancellationTokenSource _inputHandlerTokenSource = new (); + private MainLoop? _mainLoop; + + public WindowsMainLoop (IConsoleDriver consoleDriver) + { + _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); + + if (!ConsoleDriver.RunningUnitTests) + { + _winConsole = ((WindowsDriver)consoleDriver).WinConsole; + _winConsole!._mainLoop = this; + } + } + + void IMainLoopDriver.Setup (MainLoop mainLoop) + { + _mainLoop = mainLoop; + + if (ConsoleDriver.RunningUnitTests) + { + return; + } + + Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token); +#if HACK_CHECK_WINCHANGED + Task.Run (CheckWinChange); +#endif + } + + void IMainLoopDriver.Wakeup () { _eventReady.Set (); } + + bool IMainLoopDriver.EventsPending () + { + if (ConsoleDriver.RunningUnitTests) + { + return true; + } + + _waitForProbe.Set (); +#if HACK_CHECK_WINCHANGED + _winChange.Set (); +#endif + if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout)) + { + return true; + } + + try + { + if (!_eventReadyTokenSource.IsCancellationRequested) + { + // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there + // are no timers, but there IS an idle handler waiting. + _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); + } + } + catch (OperationCanceledException) + { + return true; + } + finally + { + if (!_eventReadyTokenSource.IsCancellationRequested) + { + _eventReady.Reset (); + } + } + + if (!_eventReadyTokenSource.IsCancellationRequested) + { +#if HACK_CHECK_WINCHANGED + return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged; +#else + return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); +#endif + } + + _eventReadyTokenSource.Dispose (); + _eventReadyTokenSource = new CancellationTokenSource (); + + // If cancellation was requested then always return true + return true; + } + + void IMainLoopDriver.Iteration () + { + while (!ConsoleDriver.RunningUnitTests && _resultQueue.TryDequeue (out WindowsConsole.InputRecord inputRecords)) + { + ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords); + } +#if HACK_CHECK_WINCHANGED + if (_winChanged) + { + _winChanged = false; + WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize)); + } +#endif + } + + void IMainLoopDriver.TearDown () + { + _inputHandlerTokenSource.Cancel (); + _inputHandlerTokenSource.Dispose (); + + if (_winConsole is { }) + { + var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents (); + + if (numOfEvents > 0) + { + _winConsole.FlushConsoleInputBuffer (); + //Debug.WriteLine ($"Flushed {numOfEvents} events."); + } + } + + _waitForProbe.Dispose (); + + _resultQueue.Clear (); + + _eventReadyTokenSource.Cancel (); + _eventReadyTokenSource.Dispose (); + _eventReady.Dispose (); + +#if HACK_CHECK_WINCHANGED + _winChange?.Dispose (); +#endif + + _mainLoop = null; + } + + private void WindowsInputHandler () + { + while (_mainLoop is { }) + { + try + { + if (_inputHandlerTokenSource.IsCancellationRequested) + { + try + { + _waitForProbe.Wait (_inputHandlerTokenSource.Token); + } + catch (Exception ex) + { + if (ex is OperationCanceledException or ObjectDisposedException) + { + return; + } + + throw; + } + + _waitForProbe.Reset (); + } + + ProcessInputQueue (); + } + catch (OperationCanceledException) + { + return; + } + + } + } + + private void ProcessInputQueue () + { + if (_resultQueue?.Count == 0) + { + WindowsConsole.InputRecord? result = _winConsole!.DequeueInput (); + + if (result.HasValue) + { + _resultQueue!.Enqueue (result.Value); + + _eventReady.Set (); + } + } + } + +#if HACK_CHECK_WINCHANGED + private readonly ManualResetEventSlim _winChange = new (false); + private bool _winChanged; + private Size _windowSize; + private void CheckWinChange () + { + while (_mainLoop is { }) + { + _winChange.Wait (); + _winChange.Reset (); + + // Check if the window size changed every half second. + // We do this to minimize the weird tearing seen on Windows when resizing the console + while (_mainLoop is { }) + { + Task.Delay (500).Wait (); + _windowSize = _winConsole.GetConsoleBufferWindow (out _); + + if (_windowSize != Size.Empty + && (_windowSize.Width != _consoleDriver.Cols + || _windowSize.Height != _consoleDriver.Rows)) + { + break; + } + } + + _winChanged = true; + _eventReady.Set (); + } + } +#endif +} diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs index 32c948d2e4..a36601ba1e 100644 --- a/Terminal.Gui/Drawing/Attribute.cs +++ b/Terminal.Gui/Drawing/Attribute.cs @@ -17,7 +17,7 @@ namespace Terminal.Gui; [JsonIgnore] public static Attribute Default => new (Color.White, Color.Black); - /// The -specific color value. + /// The -specific color value. [JsonIgnore (Condition = JsonIgnoreCondition.Always)] internal int PlatformColor { get; init; } diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 16af046a7f..5ce4e21df5 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -2,7 +2,7 @@ /// /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. and -/// ). +/// ). /// public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default) { diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index 8261b156a6..4a69a96b72 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -384,7 +384,7 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE // TODO: Add other resolvers }; - private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + private Cell? GetCellForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -404,7 +404,7 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE return cell; } - private Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + private Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -727,7 +727,7 @@ private abstract class IntersectionRuneResolver internal Rune _thickV; protected IntersectionRuneResolver () { SetGlyphs (); } - public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + public Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { bool useRounded = intersects.Any ( i => i?.Line.Length != 0 diff --git a/Terminal.Gui/Drawing/SixelToRender.cs b/Terminal.Gui/Drawing/SixelToRender.cs index dedd399ef9..f9981f19f7 100644 --- a/Terminal.Gui/Drawing/SixelToRender.cs +++ b/Terminal.Gui/Drawing/SixelToRender.cs @@ -2,7 +2,7 @@ /// /// Describes a request to render a given at a given . -/// Requires that the terminal and both support sixel. +/// Requires that the terminal and both support sixel. /// public class SixelToRender { diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Key.cs index 70d078e0ca..84f42ee3fd 100644 --- a/Terminal.Gui/Input/Key.cs +++ b/Terminal.Gui/Input/Key.cs @@ -713,7 +713,7 @@ out parsedInt if (GetIsKeyCodeAtoZ (keyCode) && (keyCode & KeyCode.Space) != 0) { - keyCode = keyCode & ~KeyCode.Space; + keyCode &= ~KeyCode.Space; } key = new (keyCode | modifiers); diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 5c22df0aca..a9ec754602 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -9,8 +9,8 @@ All files required to build the **Terminal.Gui** library (and NuGet package). - `Application\` - The core `Application` logic, including `Application.cs`, which is is a `static` class that provides the base 'application engine', `RunState`, and `MainLoop`. - `ConsoleDrivers\` - - `ConsoleDriver.cs` - Definition for the Console Driver API. - - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. + - `IConsoleDriver.cs` - Definition for the Console Driver API. + - Source files for the three `IConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. - `Configuration\` - Classes related the `ConfigurationManager`. diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 2938b69bc0..90152452fd 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -43,7 +43,7 @@ public TextDirection Direction set => _textDirection = EnableNeedsFormat (value); } - /// Draws the text held by to using the colors specified. + /// Draws the text held by to using the colors specified. /// /// Causes the text to be formatted (references ). Sets to /// false. @@ -59,7 +59,7 @@ public void Draw ( Attribute normalColor, Attribute hotColor, Rectangle maximum = default, - ConsoleDriver? driver = null + IConsoleDriver? driver = null ) { // With this check, we protect against subclasses with overrides of Text (like Button) diff --git a/Terminal.Gui/View/View.Layout.cs b/Terminal.Gui/View/View.Layout.cs index 1b37f9725b..b0991711b0 100644 --- a/Terminal.Gui/View/View.Layout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -557,7 +557,14 @@ public bool SetRelativeLayout (Size superviewContentSize) SetTitleTextFormatterSize (); } - SuperView?.SetNeedsDraw (); + if (SuperView is { }) + { + SuperView?.SetNeedsDraw (); + } + else + { + Application.ClearScreenNextIteration = true; + } } if (TextFormatter.ConstrainToWidth is null) diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 27fcb065af..46689dba01 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -126,7 +126,7 @@ public partial class View : IDisposable, ISupportInitializeNotification /// Points to the current driver in use by the view, it is a convenience property for simplifying the development /// of new views. /// - public static ConsoleDriver? Driver => Application.Driver; + public static IConsoleDriver? Driver => Application.Driver; /// Initializes a new instance of . /// @@ -369,7 +369,14 @@ public virtual bool Visible SetNeedsLayout (); SuperView?.SetNeedsLayout (); SetNeedsDraw (); - SuperView?.SetNeedsDraw (); + if (SuperView is { }) + { + SuperView?.SetNeedsDraw (); + } + else + { + Application.ClearScreenNextIteration = true; + } } } diff --git a/Terminal.Gui/Views/ColorPicker.Prompt.cs b/Terminal.Gui/Views/ColorPicker.Prompt.cs index 3f2372db9a..0b79d3e9ff 100644 --- a/Terminal.Gui/Views/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/ColorPicker.Prompt.cs @@ -4,7 +4,7 @@ public partial class ColorPicker { /// /// Open a with two or , based on the - /// is false or true, respectively, for + /// is false or true, respectively, for /// and colors. /// /// The title to show in the dialog. diff --git a/Terminal.Gui/Views/GraphView/Axis.cs b/Terminal.Gui/Views/GraphView/Axis.cs index c469388904..01d78ee03c 100644 --- a/Terminal.Gui/Views/GraphView/Axis.cs +++ b/Terminal.Gui/Views/GraphView/Axis.cs @@ -97,7 +97,7 @@ public HorizontalAxis () : base (Orientation.Horizontal) { } /// Text to render under the axis tick public override void DrawAxisLabel (GraphView graph, int screenPosition, string text) { - ConsoleDriver driver = Application.Driver; + IConsoleDriver driver = Application.Driver; int y = GetAxisYPosition (graph); graph.Move (screenPosition, y); diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index fdfab4d982..ac3d617977 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -608,8 +608,7 @@ public void Run (Action? action) Application.UngrabMouse (); _host.CloseAllMenus (); - Application.Driver!.ClearContents (); - Application.LayoutAndDraw (); + Application.LayoutAndDraw (true); _host.Run (action); } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 7bba8b7143..21c1a5b9f4 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -667,7 +667,7 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing return true; } - /// Gets the superview location offset relative to the location. + /// Gets the superview location offset relative to the location. /// The location offset. internal Point GetScreenOffset () { @@ -1117,7 +1117,7 @@ internal bool SelectItem (MenuItem? item) Application.UngrabMouse (); CloseAllMenus (); - Application.LayoutAndDraw (); + Application.LayoutAndDraw (true); _openedByAltKey = true; return Run (item.Action); diff --git a/Terminal.Gui/Views/TabView/TabRowView.cs b/Terminal.Gui/Views/TabView/TabRow.cs similarity index 97% rename from Terminal.Gui/Views/TabView/TabRowView.cs rename to Terminal.Gui/Views/TabView/TabRow.cs index b1ec596ecc..2ca80a69ae 100644 --- a/Terminal.Gui/Views/TabView/TabRowView.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -1,18 +1,19 @@ #nullable enable namespace Terminal.Gui; -internal class TabRowView : View +internal class TabRow : View { private readonly TabView _host; private readonly View _leftScrollIndicator; private readonly View _rightScrollIndicator; - public TabRowView (TabView host) + public TabRow (TabView host) { _host = host; - Id = "tabRowView"; + Id = "tabRow"; CanFocus = true; + TabStop = TabBehavior.TabGroup; Width = Dim.Fill (); _rightScrollIndicator = new View @@ -59,25 +60,25 @@ protected override bool OnMouseEvent (MouseEventArgs me) } } - if (!me.IsSingleDoubleOrTripleClicked) + if (me.IsWheel && !HasFocus && CanFocus) { - return false; + SetFocus (); } - if (!HasFocus && CanFocus) + if (me is { IsSingleDoubleOrTripleClicked: false, IsWheel: false }) { - SetFocus (); + return false; } - if (me.IsSingleDoubleOrTripleClicked) + if (me.IsSingleDoubleOrTripleClicked || me.IsWheel) { var scrollIndicatorHit = 0; - if (me.View is { Id: "rightScrollIndicator" }) + if (me.View is { Id: "rightScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledDown) || me.Flags.HasFlag (MouseFlags.WheeledRight)) { scrollIndicatorHit = 1; } - else if (me.View is { Id: "leftScrollIndicator" }) + else if (me.View is { Id: "leftScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledUp) || me.Flags.HasFlag (MouseFlags.WheeledLeft)) { scrollIndicatorHit = -1; } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 7916911fcd..73f286b564 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -16,7 +16,7 @@ public class TabView : View private readonly List _tabs = new (); /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. - private readonly TabRowView _tabsBar; + private readonly TabRow _tabsBar; private Tab? _selectedTab; @@ -28,7 +28,7 @@ public TabView () { CanFocus = true; TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup - _tabsBar = new TabRowView (this); + _tabsBar = new TabRow (this); _containerView = new (); ApplyStyleChanges (); @@ -518,6 +518,10 @@ internal IEnumerable CalculateViewport (Rectangle bounds) { SelectedTab?.SetFocus (); } + else + { + SelectedTab?.View?.SetFocus (); + } } /// diff --git a/Terminal.Gui/Views/TableView/TableStyle.cs b/Terminal.Gui/Views/TableView/TableStyle.cs index 4dd9477342..ffc8068805 100644 --- a/Terminal.Gui/Views/TableView/TableStyle.cs +++ b/Terminal.Gui/Views/TableView/TableStyle.cs @@ -29,7 +29,7 @@ public class TableStyle /// /// True to invert the colors of the first symbol of the selected cell in the . This gives - /// the appearance of a cursor for when the doesn't otherwise show this + /// the appearance of a cursor for when the doesn't otherwise show this /// public bool InvertSelectedCellFirstCharacter { get; set; } = false; diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 468b851f65..68c5a037e0 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1277,7 +1277,7 @@ protected virtual bool OnCellActivated (CellActivatedEventArgs args) /// /// Override to provide custom multi colouring to cells. Use to with - /// . The driver will already be in the correct place when rendering and you + /// . The driver will already be in the correct place when rendering and you /// must render the full or the view will not look right. For simpler provision of color use /// For changing the content that is rendered use /// @@ -1335,7 +1335,7 @@ internal int GetHeaderHeight () /// internal int GetHeaderHeightIfAny () { return ShouldRenderHeaders () ? GetHeaderHeight () : 0; } - private void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) + private void AddRuneAt (IConsoleDriver d, int col, int row, Rune ch) { Move (col, row); d?.AddRune (ch); diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index e2d220aceb..e7a5eb4ca4 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -73,7 +73,7 @@ public bool CanExpand () /// /// /// - public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) + public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) { List cells = new (); int? indexOfExpandCollapseSymbol = null; @@ -291,7 +291,7 @@ public virtual void FetchChildren () /// /// /// - public Rune GetExpandableSymbol (ConsoleDriver driver) + public Rune GetExpandableSymbol (IConsoleDriver driver) { Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; @@ -313,7 +313,7 @@ public Rune GetExpandableSymbol (ConsoleDriver driver) /// line body). /// /// - public virtual int GetWidth (ConsoleDriver driver) + public virtual int GetWidth (IConsoleDriver driver) { return GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length; @@ -408,7 +408,7 @@ internal void ExpandAll () /// /// /// - internal IEnumerable GetLinePrefix (ConsoleDriver driver) + internal IEnumerable GetLinePrefix (IConsoleDriver driver) { // If not showing line branches or this is a root object. if (!tree.Style.ShowBranchLines) @@ -453,7 +453,7 @@ internal IEnumerable GetLinePrefix (ConsoleDriver driver) /// /// /// - internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x) + internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x) { // if leaf node then we cannot expand if (!CanExpand ()) diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 056086c645..f66463664a 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -1000,7 +1000,7 @@ public DiscoBarSeries () protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn) { - ConsoleDriver driver = Application.Driver; + IConsoleDriver driver = Application.Driver; int x = start.X; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index ba7e3f26d7..fffa989f24 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -136,7 +136,7 @@ private static int Main (string [] args) // Process command line args // "UICatalog [--driver ] [--benchmark] [scenario name]" // If no driver is provided, the default driver is used. - Option driverOption = new Option ("--driver", "The ConsoleDriver to use.").FromAmong ( + Option driverOption = new Option ("--driver", "The IConsoleDriver to use.").FromAmong ( Application.GetDriverTypes () .Select (d => d!.Name) .ToArray () diff --git a/UnitTests/Application/ApplicationScreenTests.cs b/UnitTests/Application/ApplicationScreenTests.cs new file mode 100644 index 0000000000..c6a220e525 --- /dev/null +++ b/UnitTests/Application/ApplicationScreenTests.cs @@ -0,0 +1,68 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ApplicationTests; + +public class ApplicationScreenTests (ITestOutputHelper output) +{ + [Fact] + public void ClearScreenNextIteration_Resets_To_False_After_LayoutAndDraw () + { + // Arrange + Application.Init (); + + // Act + Application.ClearScreenNextIteration = true; + Application.LayoutAndDraw (); + + // Assert + Assert.False (Application.ClearScreenNextIteration); + + // Cleanup + Application.ResetState (true); + } + + [Fact] + public void ClearContents_Called_When_Top_Frame_Changes () + { + // Arrange + Application.Init (new FakeDriver ()); + Application.Top = new Toplevel (); + Application.TopLevels.Push (Application.Top); + + int clearedContentsRaised = 0; + + Application.Driver!.ClearedContents += (e, a) => clearedContentsRaised++; + + // Act + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (1, clearedContentsRaised); + + // Act + Application.Top.SetNeedsLayout (); + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (1, clearedContentsRaised); + + // Act + Application.Top.X = 1; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (2, clearedContentsRaised); + + // Act + Application.Top.Width = 10; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (3, clearedContentsRaised); + + // Cleanup + Application.Top.Dispose (); + Application.Top = null; + Application.Shutdown (); + } +} diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 38400d8a2a..ab44e66ef6 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -248,7 +248,7 @@ public void Init_Begin_End_Cleans_Up () [InlineData (typeof (CursesDriver))] public void Init_DriverName_Should_Pick_Correct_Driver (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driverName: driverType.Name); Assert.NotNull (Application.Driver); Assert.NotEqual (driver, Application.Driver); @@ -620,6 +620,27 @@ void Application_Iteration (object sender, IterationEventArgs e) } } + [Fact] + public void Screen_Size_Changes () + { + var driver = new FakeDriver (); + Application.Init (driver); + Assert.Equal (new (0, 0, 80, 25), driver.Screen); + Assert.Equal (new (0, 0, 80, 25), Application.Screen); + + driver.Cols = 100; + driver.Rows = 30; + // IConsoleDriver.Screen isn't assignable + //driver.Screen = new (0, 0, driver.Cols, Rows); + Assert.Equal (new (0, 0, 100, 30), driver.Screen); + Assert.NotEqual (new (0, 0, 100, 30), Application.Screen); + Assert.Equal (new (0, 0, 80, 25), Application.Screen); + Application.Screen = new (0, 0, driver.Cols, driver.Rows); + Assert.Equal (new (0, 0, 100, 30), driver.Screen); + + Application.Shutdown (); + } + private void Init () { Application.Init (new FakeDriver ()); diff --git a/UnitTests/Application/SynchronizatonContextTests.cs b/UnitTests/Application/SynchronizatonContextTests.cs index ce099fd100..8aeb36cebb 100644 --- a/UnitTests/Application/SynchronizatonContextTests.cs +++ b/UnitTests/Application/SynchronizatonContextTests.cs @@ -4,9 +4,10 @@ namespace Terminal.Gui.ApplicationTests; public class SyncrhonizationContextTests { - [Fact(Skip = "Causes ubuntu to crash in github action.")] + [Fact] public void SynchronizationContext_CreateCopy () { + ConsoleDriver.RunningUnitTests = true; Application.Init (); SynchronizationContext context = SynchronizationContext.Current; Assert.NotNull (context); @@ -20,11 +21,12 @@ public void SynchronizationContext_CreateCopy () [Theory] [InlineData (typeof (FakeDriver))] - //[InlineData (typeof (NetDriver))] + [InlineData (typeof (NetDriver))] [InlineData (typeof (WindowsDriver))] - //[InlineData (typeof (CursesDriver))] + [InlineData (typeof (CursesDriver))] public void SynchronizationContext_Post (Type driverType) { + ConsoleDriver.RunningUnitTests = true; Application.Init (driverName: driverType.Name); SynchronizationContext context = SynchronizationContext.Current; @@ -60,6 +62,7 @@ public void SynchronizationContext_Post (Type driverType) [AutoInitShutdown] public void SynchronizationContext_Send () { + ConsoleDriver.RunningUnitTests = true; Application.Init (); SynchronizationContext context = SynchronizationContext.Current; diff --git a/UnitTests/Configuration/ConfigPropertyTests.cs b/UnitTests/Configuration/ConfigPropertyTests.cs index 0bf96dc6e3..f751b3d72b 100644 --- a/UnitTests/Configuration/ConfigPropertyTests.cs +++ b/UnitTests/Configuration/ConfigPropertyTests.cs @@ -5,6 +5,8 @@ using Terminal.Gui; using Xunit; +namespace Terminal.Gui.ConfigurationTests; + public class ConfigPropertyTests { [Fact] diff --git a/UnitTests/ConsoleDrivers/AddRuneTests.cs b/UnitTests/ConsoleDrivers/AddRuneTests.cs index 6769db8300..5c753386ee 100644 --- a/UnitTests/ConsoleDrivers/AddRuneTests.cs +++ b/UnitTests/ConsoleDrivers/AddRuneTests.cs @@ -25,7 +25,7 @@ public AddRuneTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void AddRune (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.Rows = 25; diff --git a/UnitTests/ConsoleDrivers/ClipRegionTests.cs b/UnitTests/ConsoleDrivers/ClipRegionTests.cs index c85f7fec80..9e01f83ee6 100644 --- a/UnitTests/ConsoleDrivers/ClipRegionTests.cs +++ b/UnitTests/ConsoleDrivers/ClipRegionTests.cs @@ -24,7 +24,7 @@ public ClipRegionTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void AddRune_Is_Clipped (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Application.Driver!.Rows = 25; Application.Driver!.Cols = 80; @@ -62,7 +62,7 @@ public void AddRune_Is_Clipped (Type driverType) [InlineData (typeof (CursesDriver))] public void Clip_Set_To_Empty_AllInvalid (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); // Define a clip rectangle @@ -92,7 +92,7 @@ public void Clip_Set_To_Empty_AllInvalid (Type driverType) [InlineData (typeof (CursesDriver))] public void IsValidLocation (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Application.Driver!.Rows = 10; Application.Driver!.Cols = 10; diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index 8ecc978076..d75a56639d 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -25,7 +25,7 @@ public ConsoleDriverTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void End_Cleans_Up (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.End (); } @@ -34,7 +34,7 @@ public void End_Cleans_Up (Type driverType) [InlineData (typeof (FakeDriver))] public void FakeDriver_MockKeyPresses (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); var text = "MockKeyPresses"; @@ -85,7 +85,7 @@ public void FakeDriver_MockKeyPresses (Type driverType) [InlineData (typeof (FakeDriver))] public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Toplevel top = new (); @@ -124,7 +124,7 @@ public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driver [InlineData (typeof (CursesDriver))] public void Init_Inits (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); MainLoop ml = driver.Init (); Assert.NotNull (ml); Assert.NotNull (driver.Clipboard); @@ -140,7 +140,7 @@ public void Init_Inits (Type driverType) //[InlineData (typeof (FakeDriver))] //public void FakeDriver_MockKeyPresses_Press_AfterTimeOut (Type driverType) //{ - // var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + // var driver = (IConsoleDriver)Activator.CreateInstance (driverType); // Application.Init (driver); // // Simulating pressing of QuitKey after a short period of time @@ -201,7 +201,7 @@ public void Init_Inits (Type driverType) [InlineData (typeof (CursesDriver))] public void TerminalResized_Simulation (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver?.Init (); driver.Cols = 80; driver.Rows = 25; diff --git a/UnitTests/ConsoleDrivers/ContentsTests.cs b/UnitTests/ConsoleDrivers/ContentsTests.cs index 0dd4be3bfc..d0f2d83ff7 100644 --- a/UnitTests/ConsoleDrivers/ContentsTests.cs +++ b/UnitTests/ConsoleDrivers/ContentsTests.cs @@ -24,7 +24,7 @@ public ContentsTests (ITestOutputHelper output) //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed public void AddStr_Combining_Character_1st_Column (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); var expected = "\u0301!"; driver.AddStr ("\u0301!"); // acute accent + exclamation mark @@ -42,7 +42,7 @@ public void AddStr_Combining_Character_1st_Column (Type driverType) //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed public void AddStr_With_Combining_Characters (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); var acuteaccent = new Rune (0x0301); // Combining acute accent (é) @@ -98,7 +98,7 @@ public void AddStr_With_Combining_Characters (Type driverType) [InlineData (typeof (CursesDriver))] public void Move_Bad_Coordinates (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (0, driver.Col); diff --git a/UnitTests/ConsoleDrivers/DriverColorTests.cs b/UnitTests/ConsoleDrivers/DriverColorTests.cs index 8bebace1e8..856e0481a0 100644 --- a/UnitTests/ConsoleDrivers/DriverColorTests.cs +++ b/UnitTests/ConsoleDrivers/DriverColorTests.cs @@ -17,7 +17,7 @@ public class DriverColorTests [InlineData (typeof (CursesDriver))] public void Force16Colors_Sets (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.Force16Colors = true; @@ -35,7 +35,7 @@ public void Force16Colors_Sets (Type driverType) [InlineData (typeof (CursesDriver))] public void SetColors_Changes_Colors (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (ConsoleColor.Gray, Console.ForegroundColor); @@ -63,7 +63,7 @@ public void SetColors_Changes_Colors (Type driverType) [InlineData (typeof (CursesDriver), true)] public void SupportsTrueColor_Defaults (Type driverType, bool expectedSetting) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (expectedSetting, driver.SupportsTrueColor); diff --git a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs index 2380ed83e2..9720d51b5e 100644 --- a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs +++ b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs @@ -17,7 +17,7 @@ public class MainLoopDriverTests //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_AddIdle_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var idleHandlerInvoked = false; @@ -47,7 +47,7 @@ bool IdleHandler () //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var callbackInvoked = false; @@ -83,7 +83,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_IdleHandlersActive_ReturnsTrue ( Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -107,7 +107,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_NoTimersOrIdleHandlers_ReturnsFa Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -130,7 +130,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_TimersActive_ReturnsTrue ( Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -151,7 +151,7 @@ Type mainLoopDriverType //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -182,7 +182,7 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -201,7 +201,7 @@ public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -223,7 +223,7 @@ public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type ma //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -241,7 +241,7 @@ public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, T //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -261,7 +261,7 @@ public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var idleHandlerInvoked = false; @@ -287,7 +287,7 @@ public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driver //[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))] //public void MainLoop_Invoke_ValidAction_RunsAction (Type driverType, Type mainLoopDriverType) //{ - // var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + // var driver = (IConsoleDriver)Activator.CreateInstance (driverType); // var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, new object [] { driver }); // var mainLoop = new MainLoop (mainLoopDriver); // var actionInvoked = false; diff --git a/UnitTests/Drawing/SixelEncoderTests.cs b/UnitTests/Drawing/SixelEncoderTests.cs index 65d9e423af..f85942e729 100644 --- a/UnitTests/Drawing/SixelEncoderTests.cs +++ b/UnitTests/Drawing/SixelEncoderTests.cs @@ -1,6 +1,6 @@ using Color = Terminal.Gui.Color; -namespace UnitTests.Drawing; +namespace Terminal.Gui.DrawingTests; public class SixelEncoderTests { diff --git a/UnitTests/Input/EscSeqReqTests.cs b/UnitTests/Input/EscSeqReqTests.cs deleted file mode 100644 index 6b73b2af0f..0000000000 --- a/UnitTests/Input/EscSeqReqTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace Terminal.Gui.InputTests; - -public class EscSeqReqTests -{ - [Fact] - public void Add_Tests () - { - var escSeqReq = new EscSeqRequests (); - escSeqReq.Add ("t"); - Assert.Single (escSeqReq.Statuses); - Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator); - Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests); - Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding); - - escSeqReq.Add ("t", 2); - Assert.Single (escSeqReq.Statuses); - Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator); - Assert.Equal (1, escSeqReq.Statuses [^1].NumRequests); - Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding); - - escSeqReq = new EscSeqRequests (); - escSeqReq.Add ("t", 2); - Assert.Single (escSeqReq.Statuses); - Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator); - Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests); - Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding); - - escSeqReq.Add ("t", 3); - Assert.Single (escSeqReq.Statuses); - Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator); - Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests); - Assert.Equal (2, escSeqReq.Statuses [^1].NumOutstanding); - } - - [Fact] - public void Constructor_Defaults () - { - var escSeqReq = new EscSeqRequests (); - Assert.NotNull (escSeqReq.Statuses); - Assert.Empty (escSeqReq.Statuses); - } - - [Fact] - public void Remove_Tests () - { - var escSeqReq = new EscSeqRequests (); - escSeqReq.Add ("t"); - escSeqReq.Remove ("t"); - Assert.Empty (escSeqReq.Statuses); - - escSeqReq.Add ("t", 2); - escSeqReq.Remove ("t"); - Assert.Single (escSeqReq.Statuses); - Assert.Equal ("t", escSeqReq.Statuses [^1].Terminator); - Assert.Equal (2, escSeqReq.Statuses [^1].NumRequests); - Assert.Equal (1, escSeqReq.Statuses [^1].NumOutstanding); - - escSeqReq.Remove ("t"); - Assert.Empty (escSeqReq.Statuses); - } - - [Fact] - public void Requested_Tests () - { - var escSeqReq = new EscSeqRequests (); - Assert.False (escSeqReq.HasResponse ("t")); - - escSeqReq.Add ("t"); - Assert.False (escSeqReq.HasResponse ("r")); - Assert.True (escSeqReq.HasResponse ("t")); - } -} diff --git a/UnitTests/Input/EscSeqRequestsTests.cs b/UnitTests/Input/EscSeqRequestsTests.cs new file mode 100644 index 0000000000..a39dae1b15 --- /dev/null +++ b/UnitTests/Input/EscSeqRequestsTests.cs @@ -0,0 +1,188 @@ +namespace Terminal.Gui.InputTests; + +public class EscSeqRequestsTests +{ + [Fact] + public void Add_Tests () + { + EscSeqRequests.Add ("t"); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Add ("t", 2); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (3, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (3, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Clear (); + EscSeqRequests.Add ("t", 2); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (2, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (2, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Add ("t", 3); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (5, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (5, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Clear (); + } + + [Fact] + public void Constructor_Defaults () + { + Assert.NotNull (EscSeqRequests.Statuses); + Assert.Empty (EscSeqRequests.Statuses); + } + + [Fact] + public void Remove_Tests () + { + EscSeqRequests.Add ("t"); + EscSeqRequests.Remove ("t"); + Assert.Empty (EscSeqRequests.Statuses); + + EscSeqRequests.Add ("t", 2); + EscSeqRequests.Remove ("t"); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (2, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Remove ("t"); + Assert.Empty (EscSeqRequests.Statuses); + + EscSeqRequests.Clear (); + } + + [Fact] + public void HasResponse_Tests () + { + Assert.False (EscSeqRequests.HasResponse ("t")); + + EscSeqRequests.Add ("t"); + Assert.False (EscSeqRequests.HasResponse ("r")); + Assert.True (EscSeqRequests.HasResponse ("t")); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqRequests.Remove ("t"); + Assert.Empty (EscSeqRequests.Statuses); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + public void Add_Null_Or_Empty_Terminator_Throws (string terminator) + { + if (terminator is null) + { + Assert.Throws (() => EscSeqRequests.Add (terminator)); + } + else + { + Assert.Throws (() => EscSeqRequests.Add (terminator)); + } + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + public void HasResponse_Null_Or_Empty_Terminator_Does_Not_Throws (string terminator) + { + EscSeqRequests.Add ("t"); + + Assert.False (EscSeqRequests.HasResponse (terminator)); + + EscSeqRequests.Clear (); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + public void Remove_Null_Or_Empty_Terminator_Throws (string terminator) + { + EscSeqRequests.Add ("t"); + + if (terminator is null) + { + Assert.Throws (() => EscSeqRequests.Remove (terminator)); + } + else + { + Assert.Throws (() => EscSeqRequests.Remove (terminator)); + } + + EscSeqRequests.Clear (); + } + + [Fact] + public void Requests_Responses_Tests () + { + // This is simulated response from a CSI_ReportTerminalSizeInChars + ConsoleKeyInfo [] cki = + [ + new ('\u001b', 0, false, false, false), + new ('[', 0, false, false, false), + new ('8', 0, false, false, false), + new (';', 0, false, false, false), + new ('1', 0, false, false, false), + new ('0', 0, false, false, false), + new (';', 0, false, false, false), + new ('2', 0, false, false, false), + new ('0', 0, false, false, false), + new ('t', 0, false, false, false) + ]; + ConsoleKeyInfo newConsoleKeyInfo = default; + ConsoleKey key = default; + ConsoleModifiers mod = default; + + Assert.Empty (EscSeqRequests.Statuses); + + EscSeqRequests.Add ("t"); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses [^1].Terminator); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumRequests); + Assert.Equal (1, EscSeqRequests.Statuses [^1].NumOutstanding); + + EscSeqUtils.DecodeEscSeq ( + ref newConsoleKeyInfo, + ref key, + cki, + ref mod, + out string c1Control, + out string code, + out string [] values, + out string terminating, + out bool isKeyMouse, + out List mouseFlags, + out Point pos, + out bool isResponse, + null + ); + + Assert.Empty (EscSeqRequests.Statuses); + Assert.Equal (default, newConsoleKeyInfo); + Assert.Equal (default, key); + Assert.Equal (10, cki.Length); + Assert.Equal (default, mod); + Assert.Equal ("CSI", c1Control); + Assert.Null (code); + // ReSharper disable once HeuristicUnreachableCode + Assert.Equal (3, values.Length); + Assert.Equal ("8", values [0]); + Assert.Equal ("t", terminating); + Assert.False (isKeyMouse); + Assert.Single (mouseFlags); + Assert.Equal (default, mouseFlags [^1]); + Assert.Equal (Point.Empty, pos); + Assert.True (isResponse); + } +} diff --git a/UnitTests/Input/EscSeqUtilsTests.cs b/UnitTests/Input/EscSeqUtilsTests.cs index 9b811f0026..4f783115fc 100644 --- a/UnitTests/Input/EscSeqUtilsTests.cs +++ b/UnitTests/Input/EscSeqUtilsTests.cs @@ -1,4 +1,7 @@ -namespace Terminal.Gui.InputTests; +using JetBrains.Annotations; +// ReSharper disable HeuristicUnreachableCode + +namespace Terminal.Gui.InputTests; public class EscSeqUtilsTests { @@ -7,9 +10,8 @@ public class EscSeqUtilsTests private Point _arg2; private string _c1Control, _code, _terminating; private ConsoleKeyInfo [] _cki; - private EscSeqRequests _escSeqReqProc; private bool _isKeyMouse; - private bool _isReq; + private bool _isResponse; private ConsoleKey _key; private ConsoleModifiers _mod; private List _mouseFlags; @@ -19,29 +21,28 @@ public class EscSeqUtilsTests [Fact] [AutoInitShutdown] - public void DecodeEscSeq_Tests () + public void DecodeEscSeq_Multiple_Tests () { // ESC _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false) }; var expectedCki = new ConsoleKeyInfo ('\u001b', ConsoleKey.Escape, false, false, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.Escape, _key); Assert.Equal (0, (int)_mod); @@ -50,9 +51,9 @@ public void DecodeEscSeq_Tests () Assert.Null (_values); Assert.Null (_terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -61,68 +62,66 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\u0012', ConsoleKey.R, false, true, true); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.R, _key); - Assert.Equal (0, (int)_mod); + Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod); Assert.Equal ("ESC", _c1Control); Assert.Null (_code); Assert.Null (_values); Assert.Equal ("\u0012", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); ClearAll (); _cki = new ConsoleKeyInfo [] { new ('\u001b', 0, false, false, false), new ('r', 0, false, false, false) }; - expectedCki = new ('R', ConsoleKey.R, false, true, false); + expectedCki = new ('r', ConsoleKey.R, false, true, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.R, _key); - Assert.Equal (0, (int)_mod); + Assert.Equal (ConsoleModifiers.Alt, _mod); Assert.Equal ("ESC", _c1Control); Assert.Null (_code); Assert.Null (_values); Assert.Equal ("r", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -136,22 +135,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, false, false, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (0, (int)_mod); @@ -161,9 +159,9 @@ public void DecodeEscSeq_Tests () Assert.Null (_values [0]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -182,22 +180,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, true, false, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Shift, _mod); @@ -208,9 +205,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("2", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -228,22 +225,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, false, true, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Alt, _mod); @@ -254,9 +250,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -274,22 +270,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, true, true, false); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt, _mod); @@ -300,9 +295,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("4", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -320,22 +315,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, false, false, true); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Control, _mod); @@ -346,9 +340,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("5", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -366,22 +360,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, true, false, true); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Control, _mod); @@ -392,9 +385,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("6", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -412,22 +405,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, false, true, true); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod); @@ -438,9 +430,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("7", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -458,22 +450,21 @@ public void DecodeEscSeq_Tests () expectedCki = new ('\0', ConsoleKey.F3, true, true, true); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (ConsoleKey.F3, _key); Assert.Equal (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, _mod); @@ -484,9 +475,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("8", _values [^1]); Assert.Equal ("R", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -507,22 +498,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -534,9 +524,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("M", _terminating); Assert.True (_isKeyMouse); - Assert.Equal (new() { MouseFlags.Button1Pressed }, _mouseFlags); + Assert.Equal (new () { MouseFlags.Button1Pressed }, _mouseFlags); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -557,22 +547,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -587,11 +576,11 @@ public void DecodeEscSeq_Tests () Assert.Equal (2, _mouseFlags.Count); Assert.Equal ( - new() { MouseFlags.Button1Released, MouseFlags.Button1Clicked }, + new () { MouseFlags.Button1Released, MouseFlags.Button1Clicked }, _mouseFlags ); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); @@ -612,22 +601,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -639,9 +627,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("M", _terminating); Assert.True (_isKeyMouse); - Assert.Equal (new() { MouseFlags.Button1DoubleClicked }, _mouseFlags); + Assert.Equal (new () { MouseFlags.Button1DoubleClicked }, _mouseFlags); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); ClearAll (); @@ -660,22 +648,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -687,9 +674,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("M", _terminating); Assert.True (_isKeyMouse); - Assert.Equal (new() { MouseFlags.Button1TripleClicked }, _mouseFlags); + Assert.Equal (new () { MouseFlags.Button1TripleClicked }, _mouseFlags); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), WantContinuousButtonPressed = true }; var top = new Toplevel (); @@ -715,22 +702,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -742,9 +728,9 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("M", _terminating); Assert.True (_isKeyMouse); - Assert.Equal (new() { MouseFlags.Button1Pressed }, _mouseFlags); + Assert.Equal (new () { MouseFlags.Button1Pressed }, _mouseFlags); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Application.Iteration += (s, a) => { @@ -784,22 +770,21 @@ public void DecodeEscSeq_Tests () expectedCki = default (ConsoleKeyInfo); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Null (_escSeqReqProc); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -811,17 +796,17 @@ public void DecodeEscSeq_Tests () Assert.Equal ("3", _values [^1]); Assert.Equal ("m", _terminating); Assert.True (_isKeyMouse); - Assert.Equal (new() { MouseFlags.Button1Released }, _mouseFlags); + Assert.Equal (new () { MouseFlags.Button1Released }, _mouseFlags); Assert.Equal (new (1, 2), _pos); - Assert.False (_isReq); + Assert.False (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); ClearAll (); - Assert.Null (_escSeqReqProc); - _escSeqReqProc = new (); - _escSeqReqProc.Add ("t"); + Assert.Empty (EscSeqRequests.Statuses); + EscSeqRequests.Clear (); + EscSeqRequests.Add ("t"); _cki = new ConsoleKeyInfo [] { @@ -837,26 +822,25 @@ public void DecodeEscSeq_Tests () new ('t', 0, false, false, false) }; expectedCki = default (ConsoleKeyInfo); - Assert.Single (_escSeqReqProc.Statuses); - Assert.Equal ("t", _escSeqReqProc.Statuses [^1].Terminator); + Assert.Single (EscSeqRequests.Statuses); + Assert.Equal ("t", EscSeqRequests.Statuses.ToArray () [^1].Terminator); EscSeqUtils.DecodeEscSeq ( - _escSeqReqProc, - ref _newConsoleKeyInfo, - ref _key, - _cki, - ref _mod, - out _c1Control, - out _code, - out _values, - out _terminating, - out _isKeyMouse, - out _mouseFlags, - out _pos, - out _isReq, - ProcessContinuousButtonPressed - ); - Assert.Empty (_escSeqReqProc.Statuses); + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); Assert.Equal (expectedCki, _newConsoleKeyInfo); Assert.Equal (0, (int)_key); Assert.Equal (0, (int)_mod); @@ -868,11 +852,270 @@ public void DecodeEscSeq_Tests () Assert.Equal ("20", _values [^1]); Assert.Equal ("t", _terminating); Assert.False (_isKeyMouse); - Assert.Equal (new() { 0 }, _mouseFlags); + Assert.Equal (new () { 0 }, _mouseFlags); + Assert.Equal (Point.Empty, _pos); + Assert.True (_isResponse); + Assert.Equal (0, (int)_arg1); + Assert.Equal (Point.Empty, _arg2); + + ClearAll (); + } + + [Theory] + [InlineData ('A', ConsoleKey.A, true, true, false, "ESC", '\u001b', 'A')] + [InlineData ('a', ConsoleKey.A, false, true, false, "ESC", '\u001b', 'a')] + [InlineData ('\0', ConsoleKey.Spacebar, false, true, true, "ESC", '\u001b', '\0')] + [InlineData (' ', ConsoleKey.Spacebar, true, true, false, "ESC", '\u001b', ' ')] + [InlineData ('\n', ConsoleKey.Enter, false, true, true, "ESC", '\u001b', '\n')] + [InlineData ('\r', ConsoleKey.Enter, true, true, false, "ESC", '\u001b', '\r')] + public void DecodeEscSeq_More_Multiple_Tests ( + char keyChar, + ConsoleKey consoleKey, + bool shift, + bool alt, + bool control, + string c1Control, + params char [] kChars + ) + { + _cki = new ConsoleKeyInfo [kChars.Length]; + + for (var i = 0; i < kChars.Length; i++) + { + char kChar = kChars [i]; + _cki [i] = new (kChar, 0, false, false, false); + } + + var expectedCki = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control); + + EscSeqUtils.DecodeEscSeq ( + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); + Assert.Equal (expectedCki, _newConsoleKeyInfo); + Assert.Equal (consoleKey, _key); + + ConsoleModifiers mods = new (); + + if (shift) + { + mods = ConsoleModifiers.Shift; + } + + if (alt) + { + mods |= ConsoleModifiers.Alt; + } + + if (control) + { + mods |= ConsoleModifiers.Control; + } + + Assert.Equal (mods, _mod); + Assert.Equal (c1Control, _c1Control); + Assert.Null (_code); + Assert.Null (_values); + Assert.Equal (keyChar.ToString (), _terminating); + Assert.False (_isKeyMouse); + Assert.Equal (new () { 0 }, _mouseFlags); + Assert.Equal (Point.Empty, _pos); + Assert.False (_isResponse); + Assert.Equal (0, (int)_arg1); + Assert.Equal (Point.Empty, _arg2); + + ClearAll (); + } + + [Fact] + public void DecodeEscSeq_IncompleteCKInfos () + { + // This is simulated response from a CSI_ReportTerminalSizeInChars + _cki = + [ + new ('\u001b', 0, false, false, false), + new ('[', 0, false, false, false), + new ('8', 0, false, false, false), + new (';', 0, false, false, false), + new ('1', 0, false, false, false), + ]; + + ConsoleKeyInfo expectedCki = default; + + Assert.Null (EscSeqUtils.IncompleteCkInfos); + + EscSeqUtils.DecodeEscSeq ( + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); + Assert.Equal (expectedCki, _newConsoleKeyInfo); + Assert.Equal (ConsoleKey.None, _key); + Assert.Equal (ConsoleModifiers.None, _mod); + Assert.Equal ("CSI", _c1Control); + Assert.Null (_code); + Assert.Equal (2, _values.Length); + Assert.Equal ("", _terminating); + Assert.False (_isKeyMouse); + Assert.Equal ([0], _mouseFlags); + Assert.Equal (Point.Empty, _pos); + Assert.False (_isResponse); + Assert.Equal (0, (int)_arg1); + Assert.Equal (Point.Empty, _arg2); + Assert.NotNull (EscSeqUtils.IncompleteCkInfos); + Assert.Equal (_cki, EscSeqUtils.IncompleteCkInfos); + Assert.Contains (EscSeqUtils.ToString (EscSeqUtils.IncompleteCkInfos), EscSeqUtils.ToString (_cki)); + + _cki = EscSeqUtils.InsertArray ( + EscSeqUtils.IncompleteCkInfos, + [ + new ('0', 0, false, false, false), + new (';', 0, false, false, false), + new ('2', 0, false, false, false), + new ('0', 0, false, false, false), + new ('t', 0, false, false, false) + ]); + + expectedCki = default; + + // Add a request to avoid assert failure in the DecodeEscSeq method + EscSeqRequests.Add ("t"); + Assert.Single (EscSeqRequests.Statuses); + + EscSeqUtils.DecodeEscSeq ( + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); + Assert.Equal (expectedCki, _newConsoleKeyInfo); + Assert.Equal (ConsoleKey.None, _key); + + Assert.Equal (ConsoleModifiers.None, _mod); + Assert.Equal ("CSI", _c1Control); + Assert.Null (_code); + Assert.Equal (3, _values.Length); + Assert.Equal ("t", _terminating); + Assert.False (_isKeyMouse); + Assert.Equal ([0], _mouseFlags); Assert.Equal (Point.Empty, _pos); - Assert.True (_isReq); + Assert.False (EscSeqRequests.HasResponse ("t")); + Assert.True (_isResponse); Assert.Equal (0, (int)_arg1); Assert.Equal (Point.Empty, _arg2); + Assert.Null (EscSeqUtils.IncompleteCkInfos); + Assert.NotEqual (_cki, EscSeqUtils.IncompleteCkInfos); + + ClearAll (); + } + + [Theory] + [InlineData ('\u001B', ConsoleKey.Escape, false, false, false)] + [InlineData ('\r', ConsoleKey.Enter, false, false, false)] + [InlineData ('1', ConsoleKey.D1, false, false, false)] + [InlineData ('!', ConsoleKey.None, false, false, false)] + [InlineData ('a', ConsoleKey.A, false, false, false)] + [InlineData ('A', ConsoleKey.A, true, false, false)] + [InlineData ('\u0001', ConsoleKey.A, false, false, true)] + [InlineData ('\0', ConsoleKey.Spacebar, false, false, true)] + [InlineData ('\n', ConsoleKey.Enter, false, false, true)] + [InlineData ('\t', ConsoleKey.Tab, false, false, false)] + public void DecodeEscSeq_Single_Tests (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control) + { + _cki = [new (keyChar, 0, false, false, false)]; + var expectedCki = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control); + + EscSeqUtils.DecodeEscSeq ( + ref _newConsoleKeyInfo, + ref _key, + _cki, + ref _mod, + out _c1Control, + out _code, + out _values, + out _terminating, + out _isKeyMouse, + out _mouseFlags, + out _pos, + out _isResponse, + ProcessContinuousButtonPressed + ); + Assert.Empty (EscSeqRequests.Statuses); + Assert.Equal (expectedCki, _newConsoleKeyInfo); + Assert.Equal (consoleKey, _key); + + ConsoleModifiers mods = new (); + + if (shift) + { + mods = ConsoleModifiers.Shift; + } + + if (alt) + { + mods |= ConsoleModifiers.Alt; + } + + if (control) + { + mods |= ConsoleModifiers.Control; + } + + Assert.Equal (mods, _mod); + + if (keyChar == '\u001B') + { + Assert.Equal ("ESC", _c1Control); + } + else + { + Assert.Null (_c1Control); + } + + Assert.Null (_code); + Assert.Null (_values); + Assert.Null (_terminating); + Assert.False (_isKeyMouse); + Assert.Equal (new () { 0 }, _mouseFlags); + Assert.Equal (Point.Empty, _pos); + Assert.False (_isResponse); + Assert.Equal (0, (int)_arg1); + Assert.Equal (Point.Empty, _arg2); + + ClearAll (); } [Fact] @@ -916,39 +1159,39 @@ public void GetC1ControlChar_Tests () public void GetConsoleInputKey_ConsoleKeyInfo () { var cki = new ConsoleKeyInfo ('r', 0, false, false, false); - var expectedCki = new ConsoleKeyInfo ('r', 0, false, false, false); + var expectedCki = new ConsoleKeyInfo ('r', ConsoleKey.R, false, false, false); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, true, false, false); - expectedCki = new ('r', 0, true, false, false); + expectedCki = new ('r', ConsoleKey.R, true, false, false); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, false, true, false); - expectedCki = new ('r', 0, false, true, false); + expectedCki = new ('r', ConsoleKey.R, false, true, false); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, false, false, true); - expectedCki = new ('r', 0, false, false, true); + expectedCki = new ('r', ConsoleKey.R, false, false, true); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, true, true, false); - expectedCki = new ('r', 0, true, true, false); + expectedCki = new ('r', ConsoleKey.R, true, true, false); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, false, true, true); - expectedCki = new ('r', 0, false, true, true); + expectedCki = new ('r', ConsoleKey.R, false, true, true); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('r', 0, true, true, true); - expectedCki = new ('r', 0, true, true, true); + expectedCki = new ('r', ConsoleKey.R, true, true, true); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('\u0012', 0, false, false, false); - expectedCki = new ('R', ConsoleKey.R, false, false, true); + expectedCki = new ('\u0012', ConsoleKey.R, false, false, true); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('\0', (ConsoleKey)64, false, false, true); - expectedCki = new (' ', ConsoleKey.Spacebar, false, false, true); + expectedCki = new ('\0', ConsoleKey.Spacebar, false, false, true); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('\r', 0, false, false, false); @@ -960,7 +1203,7 @@ public void GetConsoleInputKey_ConsoleKeyInfo () Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); cki = new ('R', 0, false, false, false); - expectedCki = new ('R', 0, false, false, false); + expectedCki = new ('R', ConsoleKey.R, true, false, false); Assert.Equal (expectedCki, EscSeqUtils.MapConsoleKeyInfo (cki)); } @@ -995,18 +1238,19 @@ public void GetConsoleKey_Tests () Assert.Equal (ConsoleKey.F11, EscSeqUtils.GetConsoleKey ('~', "23", ref mod, ref keyChar)); Assert.Equal (ConsoleKey.F12, EscSeqUtils.GetConsoleKey ('~', "24", ref mod, ref keyChar)); Assert.Equal (0, (int)EscSeqUtils.GetConsoleKey ('~', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.Add, EscSeqUtils.GetConsoleKey ('l', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.Subtract, EscSeqUtils.GetConsoleKey ('m', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('p', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('q', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('r', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('s', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('t', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.Clear, EscSeqUtils.GetConsoleKey ('u', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('v', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('w', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('x', "", ref mod, ref keyChar)); - Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('y', "", ref mod, ref keyChar)); + // These terminators are used by macOS on a numeric keypad without keys modifiers + Assert.Equal (ConsoleKey.Add, EscSeqUtils.GetConsoleKey ('l', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Subtract, EscSeqUtils.GetConsoleKey ('m', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Insert, EscSeqUtils.GetConsoleKey ('p', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.End, EscSeqUtils.GetConsoleKey ('q', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.DownArrow, EscSeqUtils.GetConsoleKey ('r', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageDown, EscSeqUtils.GetConsoleKey ('s', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.LeftArrow, EscSeqUtils.GetConsoleKey ('t', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Clear, EscSeqUtils.GetConsoleKey ('u', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.RightArrow, EscSeqUtils.GetConsoleKey ('v', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.Home, EscSeqUtils.GetConsoleKey ('w', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.UpArrow, EscSeqUtils.GetConsoleKey ('x', null, ref mod, ref keyChar)); + Assert.Equal (ConsoleKey.PageUp, EscSeqUtils.GetConsoleKey ('y', null, ref mod, ref keyChar)); } [Fact] @@ -1027,9 +1271,9 @@ public void GetConsoleModifiers_Tests () } [Fact] - public void GetEscapeResult_Tests () + public void GetEscapeResult_Multiple_Tests () { - char [] kChars = { '\u001b', '[', '5', ';', '1', '0', 'r' }; + char [] kChars = ['\u001b', '[', '5', ';', '1', '0', 'r']; (_c1Control, _code, _values, _terminating) = EscSeqUtils.GetEscapeResult (kChars); Assert.Equal ("CSI", _c1Control); Assert.Null (_code); @@ -1037,6 +1281,35 @@ public void GetEscapeResult_Tests () Assert.Equal ("5", _values [0]); Assert.Equal ("10", _values [^1]); Assert.Equal ("r", _terminating); + + ClearAll (); + } + + [Theory] + [InlineData ('\u001B')] + [InlineData (['\r'])] + [InlineData (['1'])] + [InlineData (['!'])] + [InlineData (['a'])] + [InlineData (['A'])] + public void GetEscapeResult_Single_Tests (params char [] kChars) + { + (_c1Control, _code, _values, _terminating) = EscSeqUtils.GetEscapeResult (kChars); + + if (kChars [0] == '\u001B') + { + Assert.Equal ("ESC", _c1Control); + } + else + { + Assert.Null (_c1Control); + } + + Assert.Null (_code); + Assert.Null (_values); + Assert.Null (_terminating); + + ClearAll (); } [Fact] @@ -1073,7 +1346,7 @@ public void GetMouse_Tests () new ('M', 0, false, false, false) }; EscSeqUtils.GetMouse (cki, out List mouseFlags, out Point pos, ProcessContinuousButtonPressed); - Assert.Equal (new() { MouseFlags.Button1Pressed }, mouseFlags); + Assert.Equal (new () { MouseFlags.Button1Pressed }, mouseFlags); Assert.Equal (new (1, 2), pos); cki = new ConsoleKeyInfo [] @@ -1092,7 +1365,7 @@ public void GetMouse_Tests () Assert.Equal (2, mouseFlags.Count); Assert.Equal ( - new() { MouseFlags.Button1Released, MouseFlags.Button1Clicked }, + new () { MouseFlags.Button1Released, MouseFlags.Button1Clicked }, mouseFlags ); Assert.Equal (new (1, 2), pos); @@ -1110,7 +1383,7 @@ public void GetMouse_Tests () new ('M', 0, false, false, false) }; EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed); - Assert.Equal (new() { MouseFlags.Button1DoubleClicked }, mouseFlags); + Assert.Equal (new () { MouseFlags.Button1DoubleClicked }, mouseFlags); Assert.Equal (new (1, 2), pos); cki = new ConsoleKeyInfo [] @@ -1126,7 +1399,7 @@ public void GetMouse_Tests () new ('M', 0, false, false, false) }; EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed); - Assert.Equal (new() { MouseFlags.Button1TripleClicked }, mouseFlags); + Assert.Equal (new () { MouseFlags.Button1TripleClicked }, mouseFlags); Assert.Equal (new (1, 2), pos); cki = new ConsoleKeyInfo [] @@ -1142,7 +1415,7 @@ public void GetMouse_Tests () new ('m', 0, false, false, false) }; EscSeqUtils.GetMouse (cki, out mouseFlags, out pos, ProcessContinuousButtonPressed); - Assert.Equal (new() { MouseFlags.Button1Released }, mouseFlags); + Assert.Equal (new () { MouseFlags.Button1Released }, mouseFlags); Assert.Equal (new (1, 2), pos); } @@ -1156,9 +1429,126 @@ public void ResizeArray_ConsoleKeyInfo () Assert.Equal (cki, expectedCkInfos [0]); } + [Theory] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\b", "\b")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\t", "\t")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\n", "\n")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\r", "\r")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCe", "e")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCV", "V")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\u007f", "\u007f")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC ", " ")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\\", "\\")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC|", "|")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC1", "1")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC!", "!")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC\"", "\"")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC@", "@")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC#", "#")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC£", "£")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC$", "$")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC§", "§")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC%", "%")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC€", "€")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC&", "&")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC/", "/")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC{", "{")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC(", "(")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC[", "[")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC)", ")")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC]", "]")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC=", "=")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC}", "}")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC'", "'")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC?", "?")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC«", "«")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC»", "»")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC+", "+")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC*", "*")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC¨", "¨")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC´", "´")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC`", "`")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCç", "ç")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCº", "º")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOCª", "ª")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC~", "~")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC^", "^")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC<", "<")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC>", ">")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC,", ",")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC;", ";")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC.", ".")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC:", ":")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC-", "-")] + [InlineData ("\r[<35;50;1m[<35;49;1m[<35;47;1m[<35;46;1m[<35;45;2m[<35;44;2m[<35;43;3m[<35;42;3m[<35;41;4m[<35;40;5m[<35;39;6m[<35;49;1m[<35;48;2m[<0;33;6M[<0;33;6mOC_", "_")] + public void SplitEscapeRawString_Multiple_Tests (string rawData, string expectedLast) + { + List splitList = EscSeqUtils.SplitEscapeRawString (rawData); + Assert.Equal (18, splitList.Count); + Assert.Equal ("\r", splitList [0]); + Assert.Equal ("\u001b[<35;50;1m", splitList [1]); + Assert.Equal ("\u001b[<35;49;1m", splitList [2]); + Assert.Equal ("\u001b[<35;47;1m", splitList [3]); + Assert.Equal ("\u001b[<35;46;1m", splitList [4]); + Assert.Equal ("\u001b[<35;45;2m", splitList [5]); + Assert.Equal ("\u001b[<35;44;2m", splitList [6]); + Assert.Equal ("\u001b[<35;43;3m", splitList [7]); + Assert.Equal ("\u001b[<35;42;3m", splitList [8]); + Assert.Equal ("\u001b[<35;41;4m", splitList [9]); + Assert.Equal ("\u001b[<35;40;5m", splitList [10]); + Assert.Equal ("\u001b[<35;39;6m", splitList [11]); + Assert.Equal ("\u001b[<35;49;1m", splitList [12]); + Assert.Equal ("\u001b[<35;48;2m", splitList [13]); + Assert.Equal ("\u001b[<0;33;6M", splitList [14]); + Assert.Equal ("\u001b[<0;33;6m", splitList [15]); + Assert.Equal ("\u001bOC", splitList [16]); + Assert.Equal (expectedLast, splitList [^1]); + } + + [Theory] + [InlineData ("[<35;50;1m")] + [InlineData ("\r")] + [InlineData ("1")] + [InlineData ("!")] + [InlineData ("a")] + [InlineData ("A")] + public void SplitEscapeRawString_Single_Tests (string rawData) + { + List splitList = EscSeqUtils.SplitEscapeRawString (rawData); + Assert.Single (splitList); + Assert.Equal (rawData, splitList [0]); + } + + [Theory] + [InlineData (null, null, null, null)] + [InlineData ("\u001b[8;1", null, null, "\u001b[8;1")] + [InlineData (null, "\u001b[8;1", 5, "\u001b[8;1")] + [InlineData ("\u001b[8;1", null, 5, "\u001b[8;1")] + [InlineData ("\u001b[8;1", "0;20t", -1, "\u001b[8;10;20t")] + [InlineData ("\u001b[8;1", "0;20t", 0, "\u001b[8;10;20t")] + [InlineData ("0;20t", "\u001b[8;1", 5, "\u001b[8;10;20t")] + [InlineData ("0;20t", "\u001b[8;1", 3, "\u001b[80;20t;1")] + public void InsertArray_Tests (string toInsert, string current, int? index, string expected) + { + ConsoleKeyInfo [] toIns = EscSeqUtils.ToConsoleKeyInfoArray (toInsert); + ConsoleKeyInfo [] cki = EscSeqUtils.ToConsoleKeyInfoArray (current); + ConsoleKeyInfo [] result = EscSeqUtils.ToConsoleKeyInfoArray (expected); + + if (index is null) + { + cki = EscSeqUtils.InsertArray (toIns, cki); + } + else + { + cki = EscSeqUtils.InsertArray (toIns, cki, (int)index); + } + + Assert.Equal (result, cki); + } + private void ClearAll () { - _escSeqReqProc = default (EscSeqRequests); + EscSeqRequests.Clear (); _newConsoleKeyInfo = default (ConsoleKeyInfo); _key = default (ConsoleKey); _cki = default (ConsoleKeyInfo []); @@ -1168,7 +1558,7 @@ private void ClearAll () _terminating = default (string); _values = default (string []); _isKeyMouse = default (bool); - _isReq = default (bool); + _isResponse = false; _mouseFlags = default (List); _pos = default (Point); _arg1 = default (MouseFlags); diff --git a/UnitTests/Input/KeyTests.cs b/UnitTests/Input/KeyTests.cs index 8493e09cbe..ecc08624b5 100644 --- a/UnitTests/Input/KeyTests.cs +++ b/UnitTests/Input/KeyTests.cs @@ -342,10 +342,8 @@ public void Standard_Keys_Should_Equal_KeyCode () [InlineData ((KeyCode)'{', "{")] [InlineData ((KeyCode)'\'', "\'")] [InlineData ((KeyCode)'ó', "ó")] - [InlineData ( - (KeyCode)'Ó' | KeyCode.ShiftMask, - "Shift+Ó" - )] // TODO: This is not correct, it should be Shift+ó or just Ó + [InlineData ((KeyCode)'Ó' | KeyCode.ShiftMask, "Shift+Ó")] + [InlineData ((KeyCode)'ó' | KeyCode.ShiftMask, "Shift+ó")] [InlineData ((KeyCode)'Ó', "Ó")] [InlineData ((KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Shift+ç")] [InlineData ((KeyCode)'a', "a")] // 97 or Key.Space | Key.A diff --git a/UnitTests/LocalPackagesTests.cs b/UnitTests/LocalPackagesTests.cs index 5de7b371f3..04e5c7802b 100644 --- a/UnitTests/LocalPackagesTests.cs +++ b/UnitTests/LocalPackagesTests.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; + +namespace Terminal.Gui.BuildAndDeployTests; public class LocalPackagesTests { diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 85bbc7d1d5..3f32e35b5a 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -25,21 +25,21 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute /// /// If true, Application.Init will be called Before the test runs. /// - /// Determines which ConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver) + /// Determines which IConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver) /// will be used when Application.Init is called. If null FakeDriver will be used. Only valid if /// is true. /// /// /// If true, will force the use of . Only valid if - /// == and is true. + /// == and is true. /// /// /// Only valid if is true. Only - /// valid if == and is true. + /// valid if == and is true. /// /// /// Only valid if is true. Only valid if - /// == and is true. + /// == and is true. /// /// Determines what config file locations will load from. /// If true and is true, the test will fail. @@ -135,7 +135,7 @@ public override void Before (MethodInfo methodUnderTest) View.Instances.Clear (); } #endif - Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType)); + Application.Init ((IConsoleDriver)Activator.CreateInstance (_driverType)); } } @@ -199,6 +199,8 @@ public override void After (MethodInfo methodUnderTest) if (Application.Driver is { }) { + ((FakeDriver)Application.Driver).Rows = 25; + ((FakeDriver)Application.Driver).Cols = 25; ((FakeDriver)Application.Driver).End (); } @@ -249,12 +251,12 @@ internal partial class TestHelpers /// . /// /// - /// The ConsoleDriver to use. If null will be used. + /// The IConsoleDriver to use. If null will be used. /// public static void AssertDriverAttributesAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null, + IConsoleDriver driver = null, params Attribute [] expectedAttributes ) { @@ -319,12 +321,12 @@ params Attribute [] expectedAttributes /// Asserts that the driver contents match the expected contents, optionally ignoring any trailing whitespace. /// /// - /// The ConsoleDriver to use. If null will be used. + /// The IConsoleDriver to use. If null will be used. /// public static void AssertDriverContentsAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null, + IConsoleDriver driver = null, bool ignoreLeadingWhitespace = false ) { @@ -365,12 +367,12 @@ public static void AssertDriverContentsAre ( /// /// /// - /// The ConsoleDriver to use. If null will be used. + /// The IConsoleDriver to use. If null will be used. /// public static Rectangle AssertDriverContentsWithFrameAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null + IConsoleDriver driver = null ) { List> lines = new (); @@ -623,7 +625,7 @@ public static List GetAllViewClasses () /// /// if null uses /// - internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors) + internal static void AssertDriverUsedColors (IConsoleDriver driver = null, params Attribute [] expectedColors) { driver ??= Application.Driver; Cell [,] contents = driver.Contents; diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 15896267ea..ac12a55564 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -19,16 +19,12 @@ public void AddTab_SameTabMoreThanOnce () tv.AddTab (tab1, false); Assert.Equal (2, tv.Tabs.Count); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] + [SetupFakeDriver] public void AddTwoTabs_SecondIsSelected () { - InitFakeDriver (); - var tv = new TabView (); Tab tab1; Tab tab2; @@ -37,8 +33,6 @@ public void AddTwoTabs_SecondIsSelected () Assert.Equal (2, tv.Tabs.Count); Assert.Equal (tab2, tv.SelectedTab); - - Application.Shutdown (); } [Fact] @@ -57,9 +51,6 @@ public void EnsureSelectedTabVisible_MustScroll () // Asking to show tab2 should automatically move scroll offset accordingly tv.SelectedTab = tab2; Assert.Equal (1, tv.TabScrollOffset); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -76,8 +67,6 @@ public void EnsureSelectedTabVisible_NullSelect () Assert.Null (tv.SelectedTab); Assert.Equal (0, tv.TabScrollOffset); - - Application.Shutdown (); } [Fact] @@ -98,16 +87,13 @@ public void EnsureValidScrollOffsets_TabScrollOffset () tv.TabScrollOffset = -1; tv.SelectedTab = tab1; Assert.Equal (0, tv.TabScrollOffset); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] [AutoInitShutdown] public void MouseClick_ChangesTab () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; tv.Height = 5; @@ -115,7 +101,7 @@ public void MouseClick_ChangesTab () tv.Draw (); View tabRow = tv.Subviews [0]; - Assert.Equal ("TabRowView", tabRow.GetType ().Name); + Assert.Equal ("TabRow", tabRow.GetType ().Name); TestHelpers.AssertDriverContentsAre ( @" @@ -190,7 +176,7 @@ public void MouseClick_ChangesTab () [AutoInitShutdown] public void MouseClick_Right_Left_Arrows_ChangesTab () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 7; tv.Height = 5; @@ -198,7 +184,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab () tv.Draw (); View tabRow = tv.Subviews [0]; - Assert.Equal ("TabRowView", tabRow.GetType ().Name); + Assert.Equal ("TabRow", tabRow.GetType ().Name); TestHelpers.AssertDriverContentsAre ( @" @@ -274,7 +260,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab () [AutoInitShutdown] public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 9; tv.Height = 7; @@ -286,7 +272,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () tv.Draw (); View tabRow = tv.Subviews [0]; - Assert.Equal ("TabRowView", tabRow.GetType ().Name); + Assert.Equal ("TabRow", tabRow.GetType ().Name); TestHelpers.AssertDriverContentsAre ( @" @@ -366,9 +352,9 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () [Fact] [AutoInitShutdown] - public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () + public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp_F6 () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 7; tv.Height = 5; @@ -393,7 +379,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tv.SelectedTab.View, top.Focused.MostFocused); // Press the cursor up key to focus the selected tab - Application.RaiseKeyDownEvent (Key.CursorUp); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); Application.LayoutAndDraw (); // Is the selected tab focused @@ -411,7 +397,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () }; // Press the cursor right key to select the next tab - Application.RaiseKeyDownEvent (Key.CursorRight); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); @@ -420,7 +406,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the cursor down key. Since the selected tab has no focusable views, the focus should move to the next view in the toplevel - Application.RaiseKeyDownEvent (Key.CursorDown); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (btn, top.MostFocused); @@ -436,40 +422,55 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.False (tv.SelectedTab.View.CanFocus); // Press cursor up. Should focus the subview in the selected tab. - Application.RaiseKeyDownEvent (Key.CursorUp); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); Assert.Equal (tab2, tv.SelectedTab); Assert.NotEqual (btnSubView, top.MostFocused); Assert.Equal (tab2, top.MostFocused); tv.SelectedTab.View.CanFocus = true; - Application.RaiseKeyDownEvent (Key.CursorDown); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (btnSubView, top.MostFocused); - Application.RaiseKeyDownEvent (Key.CursorUp); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); + // TabRow now has TabGroup which only F6 is allowed + Assert.NotEqual (tab2, top.MostFocused); + Assert.Equal (btn, top.MostFocused); + + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); + Assert.Equal (btnSubView, top.MostFocused); + + Assert.True (Application.RaiseKeyDownEvent (Key.F6)); Assert.Equal (tab2, top.MostFocused); // Press the cursor down key twice. - Application.RaiseKeyDownEvent (Key.CursorDown); - Application.RaiseKeyDownEvent (Key.CursorDown); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (btn, top.MostFocused); // Press the cursor down key again will focus next view in the toplevel, which is the TabView - Application.RaiseKeyDownEvent (Key.CursorDown); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (tv, top.Focused); // Due to the RestoreFocus method prioritize the _previouslyFocused, so btnSubView will be focused again Assert.Equal (btnSubView, tv.MostFocused); // Press the cursor up key to focus the selected tab which it's the only way to do that - Application.RaiseKeyDownEvent (Key.CursorUp); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (btn, top.Focused); + + Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); + Assert.Equal (tv, top.Focused); + Assert.Equal (btnSubView, top.MostFocused); + + Assert.True (Application.RaiseKeyDownEvent (Key.F6)); Assert.Equal (tv, top.Focused); Assert.Equal (tab2, top.Focused.MostFocused); Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the cursor left key to select the previous tab - Application.RaiseKeyDownEvent (Key.CursorLeft); + Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); @@ -479,7 +480,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tab1, top.Focused.MostFocused); // Press the end key to select the last tab - Application.RaiseKeyDownEvent (Key.End); + Assert.True (Application.RaiseKeyDownEvent (Key.End)); Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); @@ -488,7 +489,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the home key to select the first tab - Application.RaiseKeyDownEvent (Key.Home); + Assert.True (Application.RaiseKeyDownEvent (Key.Home)); Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); @@ -497,7 +498,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the page down key to select the next set of tabs - Application.RaiseKeyDownEvent (Key.PageDown); + Assert.True (Application.RaiseKeyDownEvent (Key.PageDown)); Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); @@ -506,7 +507,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the page up key to select the previous set of tabs - Application.RaiseKeyDownEvent (Key.PageUp); + Assert.True (Application.RaiseKeyDownEvent (Key.PageUp)); Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); @@ -526,9 +527,6 @@ public void RemoveAllTabs_ClearsSelection () tv.RemoveTab (tab2); Assert.Null (tv.SelectedTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -540,9 +538,6 @@ public void RemoveTab_ChangesSelection () tv.RemoveTab (tab1); Assert.Equal (tab2, tv.SelectedTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -560,9 +555,6 @@ public void RemoveTab_MultipleCalls_NotAnError () tv.RemoveTab (tab1); Assert.Equal (tab2, tv.SelectedTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -588,16 +580,13 @@ public void SelectedTabChanged_Called () Assert.Equal (1, called); Assert.Equal (tab1, oldTab); Assert.Equal (tab2, newTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; tv.Style = new () { ShowTopLine = false }; @@ -621,7 +610,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; tv.Style = new () { ShowTopLine = false }; @@ -645,7 +634,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; tv.Style = new () { ShowTopLine = false }; @@ -671,7 +660,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( ); tv.SelectedTab = tab2; - Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused); + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRow")).MostFocused); tv.Layout (); View.SetClipToScreen (); @@ -744,7 +733,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; @@ -768,7 +757,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; @@ -792,7 +781,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; @@ -817,7 +806,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () ); tv.SelectedTab = tab2; - Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused); + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRow")).MostFocused); tv.Layout (); View.SetClipToScreen (); @@ -893,7 +882,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; tv.Layout (); @@ -915,7 +904,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width4 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; tv.Layout (); @@ -938,7 +927,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width4 () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; @@ -1035,7 +1024,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; tv.Height = 5; @@ -1077,7 +1066,7 @@ public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; @@ -1101,7 +1090,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () { - TabView tv = GetTabView (out _, out _, false); + TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; @@ -1125,7 +1114,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; @@ -1207,7 +1196,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; @@ -1279,9 +1268,6 @@ public void SwitchTabBy_NormalUsage () // even though we go right 2 indexes the event should only be called once Assert.Equal (1, called); Assert.Equal (tab4, tv.SelectedTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -1297,9 +1283,6 @@ public void SwitchTabBy_OutOfTabsRange () tv.SwitchTabBy (-500); Assert.Equal (tab1, tv.SelectedTab); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] @@ -1318,16 +1301,13 @@ public void RemoveTab_ThatHasFocus () } Assert.Empty (tv.Tabs); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] [SetupFakeDriver] public void Add_Three_TabsOnTop_ChangesTab () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; tv.AddTab ( @@ -1392,7 +1372,7 @@ public void Add_Three_TabsOnTop_ChangesTab () [SetupFakeDriver] public void Add_Three_TabsOnBottom_ChangesTab () { - TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; tv.AddTab ( @@ -1455,15 +1435,59 @@ public void Add_Three_TabsOnBottom_ChangesTab () ); } - private TabView GetTabView () { return GetTabView (out _, out _); } + [Fact] + [AutoInitShutdown] + public void Tab_Get_Focus_By_Press_F6 () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + + tv.Width = 20; + tv.Height = 5; + + Toplevel top = new (); + top.Add (tv); + Application.Begin (top); - private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = true) + Assert.False (tab1.HasFocus); + + Assert.True (Application.RaiseKeyDownEvent (Key.F6)); + Assert.True (tab1.HasFocus); + top.Dispose (); + } + + [Fact] + [SetupFakeDriver] + public void Mouse_Wheel_Changes_Tab () { - if (initFakeDriver) - { - InitFakeDriver (); - } + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + + tv.Width = 20; + tv.Height = 5; + + Toplevel top = new (); + top.Add (tv); + Application.Begin (top); + Assert.False (tab1.HasFocus); + + Application.RaiseMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.WheeledDown }); + Assert.True (tab2.HasFocus); + + Application.RaiseMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.WheeledUp }); + Assert.True (tab1.HasFocus); + + Application.RaiseMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.WheeledRight }); + Assert.True (tab2.HasFocus); + + Application.RaiseMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.WheeledLeft }); + Assert.True (tab1.HasFocus); + top.Dispose (); + } + + private TabView GetTabView () { return GetTabView (out _, out _); } + + private TabView GetTabView (out Tab tab1, out Tab tab2) + { var tv = new TabView () { Id = "tv " }; tv.BeginInit (); tv.EndInit (); @@ -1477,14 +1501,4 @@ private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = tr return tv; } - - private void InitFakeDriver () - { - ConfigurationManager.Locations = ConfigLocations.Default; - ConfigurationManager.Reset (); - - var driver = new FakeDriver (); - Application.Init (driver); - driver.Init (); - } } diff --git a/bench.json b/bench.json deleted file mode 100644 index 52a38240e1..0000000000 --- a/bench.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "Scenario": "Adornments Demo", - "Duration": "00:00:00.1805368", - "IterationCount": 501, - "ClearedContentCount": 0, - "RefreshedCount": 503, - "UpdatedCount": 1, - "DrawCompleteCount": 82, - "LaidOutCount": 82 - }, - { - "Scenario": "All Views Tester", - "Duration": "00:00:00.1070009", - "IterationCount": 501, - "ClearedContentCount": 0, - "RefreshedCount": 503, - "UpdatedCount": 1, - "DrawCompleteCount": 103, - "LaidOutCount": 182 - }, - { - "Scenario": "Animation", - "Duration": "00:00:00.0675802", - "IterationCount": 501, - "ClearedContentCount": 0, - "RefreshedCount": 503, - "UpdatedCount": 1, - "DrawCompleteCount": 4, - "LaidOutCount": 4 - }, - { - "Scenario": "Arrangement", - "Duration": "00:00:00.1284709", - "IterationCount": 501, - "ClearedContentCount": 0, - "RefreshedCount": 503, - "UpdatedCount": 1, - "DrawCompleteCount": 123, - "LaidOutCount": 123 - }, - { - "Scenario": "ASCIICustomButtonTest", - "Duration": "00:00:01.0613372", - "IterationCount": 30, - "ClearedContentCount": 0, - "RefreshedCount": 32, - "UpdatedCount": 31, - "DrawCompleteCount": 4185, - "LaidOutCount": 2852 - } -] \ No newline at end of file