//
// FakeDriver.cs: A fake ConsoleDriver for unit tests.
//
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Text;
// Alias Console to MockConsole so we don't accidentally use Console
using Console = Terminal.Gui.FakeConsole;
using Unix.Terminal;
using static Terminal.Gui.WindowsConsole;
using System.Drawing;
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 ()
{
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 ();
}
}
}
}
public override void End ()
{
FakeConsole.ResetColor ();
FakeConsole.Clear ();
}
public override void Init (Action terminalResized)
{
FakeConsole.MockKeyPresses.Clear ();
TerminalResized = terminalResized;
Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH;
Rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
FakeConsole.Clear ();
ResizeScreen ();
// Call InitializeColorSchemes before UpdateOffScreen as it references Colors
CurrentAttribute = MakeColor (Color.White, Color.Black);
InitializeColorSchemes ();
ClearContents ();
}
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;
FakeConsole.BackgroundColor = (ConsoleColor)attr.Background;
}
outputWidth++;
var rune = (Rune)Contents [row, col].Runes [0];
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
// Cache the list of ConsoleColor values.
private static readonly HashSet ConsoleColorValues = new HashSet (
Enum.GetValues (typeof (ConsoleColor)).OfType ().Select (c => (int)c)
);
void SetColor (int color)
{
if (ConsoleColorValues.Contains (color & 0xffff)) {
FakeConsole.BackgroundColor = (ConsoleColor)(color & 0xffff);
}
if (ConsoleColorValues.Contains ((color >> 16) & 0xffff)) {
FakeConsole.ForegroundColor = (ConsoleColor)((color >> 16) & 0xffff);
}
}
///
/// In the FakeDriver, colors are encoded as an int; same as NetDriver
/// Extracts the foreground and background colors from the encoded value.
/// Assumes a 4-bit encoded value for both foreground and background colors.
///
internal override void GetColors (int value, out Color foreground, out Color background)
{
// Assume a 4-bit encoded value for both foreground and background colors.
foreground = (Color)((value >> 16) & 0xF);
background = (Color)(value & 0xF);
}
///
/// 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 (
value: ((((int)foreground) & 0xffff) << 16) | (((int)background) & 0xffff),
foreground: foreground,
background: background
);
}
#endregion
public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
{
if (consoleKeyInfo.Key != ConsoleKey.Packet) {
return consoleKeyInfo;
}
var mod = consoleKeyInfo.Modifiers;
var shift = (mod & ConsoleModifiers.Shift) != 0;
var alt = (mod & ConsoleModifiers.Alt) != 0;
var control = (mod & ConsoleModifiers.Control) != 0;
var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _);
return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control);
}
Key MapKey (ConsoleKeyInfo keyInfo)
{
switch (keyInfo.Key) {
case ConsoleKey.Escape:
return MapKeyModifiers (keyInfo, Key.Esc);
case ConsoleKey.Tab:
return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab;
case ConsoleKey.Clear:
return MapKeyModifiers (keyInfo, Key.Clear);
case ConsoleKey.Home:
return MapKeyModifiers (keyInfo, Key.Home);
case ConsoleKey.End:
return MapKeyModifiers (keyInfo, Key.End);
case ConsoleKey.LeftArrow:
return MapKeyModifiers (keyInfo, Key.CursorLeft);
case ConsoleKey.RightArrow:
return MapKeyModifiers (keyInfo, Key.CursorRight);
case ConsoleKey.UpArrow:
return MapKeyModifiers (keyInfo, Key.CursorUp);
case ConsoleKey.DownArrow:
return MapKeyModifiers (keyInfo, Key.CursorDown);
case ConsoleKey.PageUp:
return MapKeyModifiers (keyInfo, Key.PageUp);
case ConsoleKey.PageDown:
return MapKeyModifiers (keyInfo, Key.PageDown);
case ConsoleKey.Enter:
return MapKeyModifiers (keyInfo, Key.Enter);
case ConsoleKey.Spacebar:
return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar);
case ConsoleKey.Backspace:
return MapKeyModifiers (keyInfo, Key.Backspace);
case ConsoleKey.Delete:
return MapKeyModifiers (keyInfo, Key.DeleteChar);
case ConsoleKey.Insert:
return MapKeyModifiers (keyInfo, Key.InsertChar);
case ConsoleKey.PrintScreen:
return MapKeyModifiers (keyInfo, Key.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 Key.Unknown;
}
return (Key)((uint)keyInfo.KeyChar);
}
var key = keyInfo.Key;
if (key >= ConsoleKey.A && key <= ConsoleKey.Z) {
var delta = key - ConsoleKey.A;
if (keyInfo.Modifiers == ConsoleModifiers.Control) {
return (Key)(((uint)Key.CtrlMask) | ((uint)Key.A + delta));
}
if (keyInfo.Modifiers == ConsoleModifiers.Alt) {
return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta));
}
if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta));
}
if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
if (keyInfo.KeyChar == 0) {
return (Key)(((uint)Key.AltMask | (uint)Key.CtrlMask) | ((uint)Key.A + delta));
} else {
return (Key)((uint)keyInfo.KeyChar);
}
}
return (Key)((uint)keyInfo.KeyChar);
}
if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) {
var delta = key - ConsoleKey.D0;
if (keyInfo.Modifiers == ConsoleModifiers.Alt) {
return (Key)(((uint)Key.AltMask) | ((uint)Key.D0 + delta));
}
if (keyInfo.Modifiers == ConsoleModifiers.Control) {
return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta));
}
if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) {
return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
}
if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) {
return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta));
}
}
return (Key)((uint)keyInfo.KeyChar);
}
if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) {
var delta = key - ConsoleKey.F1;
if ((keyInfo.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
return MapKeyModifiers (keyInfo, (Key)((uint)Key.F1 + delta));
}
return (Key)((uint)Key.F1 + delta);
}
if (keyInfo.KeyChar != 0) {
return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar));
}
return (Key)(0xffffffff);
}
KeyModifiers keyModifiers;
private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key)
{
Key keyMod = new Key ();
if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) {
keyMod = Key.ShiftMask;
}
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) {
keyMod |= Key.CtrlMask;
}
if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) {
keyMod |= Key.AltMask;
}
return keyMod != Key.Null ? keyMod | key : key;
}
Action _keyDownHandler;
Action _keyHandler;
Action _keyUpHandler;
private CursorVisibility _savedCursorVisibility;
public override void PrepareToRun (MainLoop mainLoop, Action keyHandler, Action keyDownHandler, Action keyUpHandler, Action mouseHandler)
{
_keyDownHandler = keyDownHandler;
_keyHandler = keyHandler;
_keyUpHandler = keyUpHandler;
// Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called
(mainLoop.MainLoopDriver as FakeMainLoop).KeyPressed += (consoleKey) => ProcessInput (consoleKey);
}
void ProcessInput (ConsoleKeyInfo consoleKey)
{
if (consoleKey.Key == ConsoleKey.Packet) {
consoleKey = FromVKPacketToKConsoleKeyInfo (consoleKey);
}
keyModifiers = new KeyModifiers ();
if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Shift)) {
keyModifiers.Shift = true;
}
if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Alt)) {
keyModifiers.Alt = true;
}
if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Control)) {
keyModifiers.Ctrl = true;
}
var map = MapKey (consoleKey);
if (map == (Key)0xffffffff) {
if ((consoleKey.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) {
_keyDownHandler (new KeyEvent (map, keyModifiers));
_keyUpHandler (new KeyEvent (map, keyModifiers));
}
return;
}
_keyDownHandler (new KeyEvent (map, keyModifiers));
_keyHandler (new KeyEvent (map, keyModifiers));
_keyUpHandler (new KeyEvent (map, keyModifiers));
}
///
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)
{
ProcessInput (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 ();
TerminalResized?.Invoke ();
}
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 ()
{
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
}