// // FakeDriver.cs: A fake ConsoleDriver for unit tests. // using System.Diagnostics; using System.Runtime.InteropServices; using Terminal.Gui.ConsoleDrivers; // Alias Console to MockConsole so we don't accidentally use Console namespace Terminal.Gui; /// Implements a mock ConsoleDriver for unit testing public class FakeDriver : ConsoleDriver { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public class Behaviors { public Behaviors ( bool useFakeClipboard = false, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false ) { UseFakeClipboard = useFakeClipboard; FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException; FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue; // double check usage is correct Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false); Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false); } public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; } public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; } public bool UseFakeClipboard { get; internal set; } } public static Behaviors FakeBehaviors = new (); public override bool SupportsTrueColor => false; public FakeDriver () { Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH; Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT; if (FakeBehaviors.UseFakeClipboard) { Clipboard = new FakeClipboard ( FakeBehaviors.FakeClipboardAlwaysThrowsNotSupportedException, FakeBehaviors.FakeClipboardIsSupportedAlwaysFalse ); } else { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { 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 (); } } } } internal override void End () { FakeConsole.ResetColor (); FakeConsole.Clear (); } private FakeMainLoop _mainLoopDriver; internal override MainLoop Init () { FakeConsole.MockKeyPresses.Clear (); Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH; Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT; FakeConsole.Clear (); ResizeScreen (); CurrentAttribute = new Attribute (Color.White, Color.Black); //ClearContents (); _mainLoopDriver = new FakeMainLoop (this); _mainLoopDriver.MockKeyPressed = MockKeyPressedHandler; return new MainLoop (_mainLoopDriver); } public override bool UpdateScreen () { bool updated = false; int savedRow = FakeConsole.CursorTop; int savedCol = FakeConsole.CursorLeft; bool savedCursorVisible = FakeConsole.CursorVisible; var top = 0; var left = 0; int rows = Rows; int cols = Cols; var output = new StringBuilder (); var redrawAttr = new Attribute (); int lastCol = -1; for (int row = top; row < rows; row++) { if (!_dirtyLines [row]) { continue; } updated = true; FakeConsole.CursorTop = row; FakeConsole.CursorLeft = 0; _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; FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 (); FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 (); } outputWidth++; Rune rune = Contents [row, col].Rune; output.Append (rune.ToString ()); if (rune.IsSurrogatePair () && rune.GetColumns () < 2) { WriteToConsole (output, ref lastCol, row, ref outputWidth); FakeConsole.CursorLeft--; } Contents [row, col].IsDirty = false; } } if (output.Length > 0) { FakeConsole.CursorTop = row; FakeConsole.CursorLeft = lastCol; foreach (char c in output.ToString ()) { FakeConsole.Write (c); } } } FakeConsole.CursorTop = 0; FakeConsole.CursorLeft = 0; //SetCursorVisibility (savedVisibility); void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { FakeConsole.CursorTop = row; FakeConsole.CursorLeft = lastCol; foreach (char c in output.ToString ()) { FakeConsole.Write (c); } output.Clear (); lastCol += outputWidth; outputWidth = 0; } FakeConsole.CursorTop = savedRow; FakeConsole.CursorLeft = savedCol; FakeConsole.CursorVisible = savedCursorVisible; return updated; } #region Color Handling ///// ///// In the FakeDriver, colors are encoded as an int; same as NetDriver ///// 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: 0,//((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff), // foreground: foreground, // background: background // ); //} #endregion private KeyCode MapKey (ConsoleKeyInfo keyInfo) { switch (keyInfo.Key) { case ConsoleKey.Escape: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Esc); case ConsoleKey.Tab: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Tab); case ConsoleKey.Clear: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Clear); case ConsoleKey.Home: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Home); case ConsoleKey.End: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.End); case ConsoleKey.LeftArrow: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorLeft); case ConsoleKey.RightArrow: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorRight); case ConsoleKey.UpArrow: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorUp); case ConsoleKey.DownArrow: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.CursorDown); case ConsoleKey.PageUp: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageUp); case ConsoleKey.PageDown: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PageDown); case ConsoleKey.Enter: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Enter); case ConsoleKey.Spacebar: return ConsoleKeyMapping.MapToKeyCodeModifiers ( keyInfo.Modifiers, keyInfo.KeyChar == 0 ? KeyCode.Space : (KeyCode)keyInfo.KeyChar ); case ConsoleKey.Backspace: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Backspace); case ConsoleKey.Delete: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Delete); case ConsoleKey.Insert: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.Insert); case ConsoleKey.PrintScreen: return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode.PrintScreen); 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.OemPeriod: case ConsoleKey.OemComma: case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: if (keyInfo.KeyChar == 0) { return KeyCode.Null; } return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); } ConsoleKey key = keyInfo.Key; if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { int delta = key - ConsoleKey.A; if (keyInfo.KeyChar != (uint)key) { return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.Key); } if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift)) { return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)KeyCode.A + delta)); } char alphaBase = keyInfo.Modifiers != ConsoleModifiers.Shift ? 'A' : 'a'; return (KeyCode)((uint)alphaBase + delta); } return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); } private CursorVisibility _savedCursorVisibility; private void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo) { if (consoleKeyInfo.Key == ConsoleKey.Packet) { consoleKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo); } KeyCode map = MapKey (consoleKeyInfo); OnKeyDown (new Key (map)); OnKeyUp (new Key (map)); //OnKeyPressed (new KeyEventArgs (map)); } /// public override bool GetCursorVisibility (out CursorVisibility visibility) { visibility = FakeConsole.CursorVisible ? CursorVisibility.Default : CursorVisibility.Invisible; return FakeConsole.CursorVisible; } /// public override bool SetCursorVisibility (CursorVisibility visibility) { _savedCursorVisibility = visibility; return FakeConsole.CursorVisible = visibility == CursorVisibility.Default; } /// public override bool EnsureCursorVisibility () { if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows)) { GetCursorVisibility (out CursorVisibility cursorVisibility); _savedCursorVisibility = cursorVisibility; SetCursorVisibility (CursorVisibility.Invisible); return false; } SetCursorVisibility (_savedCursorVisibility); return FakeConsole.CursorVisible; } public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) { MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); } public void SetBufferSize (int width, int height) { FakeConsole.SetBufferSize (width, height); Cols = width; Rows = height; SetWindowSize (width, height); ProcessResize (); } public void SetWindowSize (int width, int height) { FakeConsole.SetWindowSize (width, height); if (width != Cols || height != Rows) { SetBufferSize (width, height); Cols = width; Rows = height; } ProcessResize (); } public void SetWindowPosition (int left, int top) { if (Left > 0 || Top > 0) { Left = 0; Top = 0; } FakeConsole.SetWindowPosition (Left, Top); } private void ProcessResize () { ResizeScreen (); ClearContents (); OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); } public virtual void ResizeScreen () { if (FakeConsole.WindowHeight > 0) { // Can raise an exception while it is still resizing. try { FakeConsole.CursorTop = 0; FakeConsole.CursorLeft = 0; FakeConsole.WindowTop = 0; FakeConsole.WindowLeft = 0; } catch (IOException) { return; } catch (ArgumentOutOfRangeException) { return; } } // CONCURRENCY: Unsynchronized access to Clip is not safe. Clip = new (new (0, 0, Cols, Rows)); } public override void UpdateCursor () { if (!EnsureCursorVisibility ()) { return; } // Prevents the exception of size changing during resizing. try { // BUGBUG: Why is this using BufferWidth/Height and now Cols/Rows? if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight) { FakeConsole.SetCursorPosition (Col, Row); } } catch (IOException) { } catch (ArgumentOutOfRangeException) { } } #region Not Implemented public override void Suspend () { //throw new NotImplementedException (); } #endregion public class FakeClipboard : ClipboardBase { public Exception FakeException; private readonly bool _isSupportedAlwaysFalse; private string _contents = string.Empty; public FakeClipboard ( bool fakeClipboardThrowsNotSupportedException = false, bool isSupportedAlwaysFalse = false ) { _isSupportedAlwaysFalse = isSupportedAlwaysFalse; if (fakeClipboardThrowsNotSupportedException) { FakeException = new NotSupportedException ("Fake clipboard exception"); } } public override bool IsSupported => !_isSupportedAlwaysFalse; protected override string GetClipboardDataImpl () { if (FakeException is { }) { throw FakeException; } return _contents; } protected override void SetClipboardDataImpl (string text) { if (text is null) { throw new ArgumentNullException (nameof (text)); } if (FakeException is { }) { throw FakeException; } _contents = text; } } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member }