//
// FakeDriver.cs: A fake ConsoleDriver for unit tests.
//
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Terminal.Gui.ConsoleDrivers;
// Alias Console to MockConsole so we don't accidentally use Console
using Console = Terminal.Gui.FakeConsole;
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 bool UseFakeClipboard { get; internal set; }
public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; }
public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; }
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 static FakeDriver.Behaviors FakeBehaviors = new Behaviors ();
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 ();
}
FakeMainLoop _mainLoopDriver = null;
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 void UpdateScreen ()
{
var savedRow = FakeConsole.CursorTop;
var savedCol = FakeConsole.CursorLeft;
var savedCursorVisible = FakeConsole.CursorVisible;
var top = 0;
var left = 0;
var rows = Rows;
var cols = Cols;
System.Text.StringBuilder output = new System.Text.StringBuilder ();
Attribute redrawAttr = new Attribute ();
var lastCol = -1;
for (var row = top; row < rows; row++) {
if (!_dirtyLines [row]) {
continue;
}
FakeConsole.CursorTop = row;
FakeConsole.CursorLeft = 0;
_dirtyLines [row] = false;
output.Clear ();
for (var 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.ColorName;
FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.ColorName;
}
outputWidth++;
var 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 (var c in output.ToString ()) {
FakeConsole.Write (c);
}
}
}
FakeConsole.CursorTop = 0;
FakeConsole.CursorLeft = 0;
//SetCursorVisibility (savedVisibitity);
void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
{
FakeConsole.CursorTop = row;
FakeConsole.CursorLeft = lastCol;
foreach (var c in output.ToString ()) {
FakeConsole.Write (c);
}
output.Clear ();
lastCol += outputWidth;
outputWidth = 0;
}
FakeConsole.CursorTop = savedRow;
FakeConsole.CursorLeft = savedCol;
FakeConsole.CursorVisible = savedCursorVisible;
}
public override void Refresh ()
{
UpdateScreen ();
UpdateCursor ();
}
#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
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)((uint)keyInfo.KeyChar));
}
var key = keyInfo.Key;
if (key >= ConsoleKey.A && key <= ConsoleKey.Z) {
var delta = key - ConsoleKey.A;
if (keyInfo.KeyChar != (uint)key) {
return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
}
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));
}
var alphaBase = ((keyInfo.Modifiers != ConsoleModifiers.Shift)) ? 'A' : 'a';
return (KeyCode)((uint)alphaBase + delta);
}
return ConsoleKeyMapping.MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar));
}
private CursorVisibility _savedCursorVisibility;
void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo)
{
if (consoleKeyInfo.Key == ConsoleKey.Packet) {
consoleKeyInfo = ConsoleKeyMapping.DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
}
var 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);
}
void ProcessResize ()
{
ResizeScreen ();
ClearContents ();
OnSizeChanged (new SizeChangedEventArgs (new Size (Cols, Rows)));
}
public virtual void ResizeScreen ()
{
if (FakeConsole.WindowHeight > 0) {
// Can raise an exception while is still resizing.
try {
FakeConsole.CursorTop = 0;
FakeConsole.CursorLeft = 0;
FakeConsole.WindowTop = 0;
FakeConsole.WindowLeft = 0;
} catch (System.IO.IOException) {
return;
} catch (ArgumentOutOfRangeException) {
return;
}
}
Clip = new Rect (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 (System.IO.IOException) { } catch (ArgumentOutOfRangeException) { }
}
#region Not Implemented
public override void Suspend ()
{
return;
//throw new NotImplementedException ();
}
#endregion
public class FakeClipboard : ClipboardBase {
public Exception FakeException = null;
string _contents = string.Empty;
bool _isSupportedAlwaysFalse = false;
public override bool IsSupported => !_isSupportedAlwaysFalse;
public FakeClipboard (bool fakeClipboardThrowsNotSupportedException = false, bool isSupportedAlwaysFalse = false)
{
_isSupportedAlwaysFalse = isSupportedAlwaysFalse;
if (fakeClipboardThrowsNotSupportedException) {
FakeException = new NotSupportedException ("Fake clipboard exception");
}
}
protected override string GetClipboardDataImpl ()
{
if (FakeException != null) {
throw FakeException;
}
return _contents;
}
protected override void SetClipboardDataImpl (string text)
{
if (text == null) {
throw new ArgumentNullException (nameof (text));
}
if (FakeException != null) {
throw FakeException;
}
_contents = text;
}
}
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}