Files
Terminal.Gui/Terminal.Gui/ConsoleDrivers/NetDriver.cs
2023-02-06 11:25:29 +09:00

2148 lines
63 KiB
C#

//#define PROCESS_REQUEST
//
// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
//
// Authors:
// Miguel de Icaza (miguel@gnome.org)
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NStack;
namespace Terminal.Gui {
internal class NetWinVTConsole {
IntPtr InputHandle, OutputHandle, ErrorHandle;
uint originalInputConsoleMode, originalOutputConsoleMode, originalErrorConsoleMode;
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 ()}.");
}
}
const int STD_INPUT_HANDLE = -10;
const int STD_OUTPUT_HANDLE = -11;
const int STD_ERROR_HANDLE = -12;
// Input modes.
const uint ENABLE_PROCESSED_INPUT = 1;
const uint ENABLE_LINE_INPUT = 2;
const uint ENABLE_ECHO_INPUT = 4;
const uint ENABLE_WINDOW_INPUT = 8;
const uint ENABLE_MOUSE_INPUT = 16;
const uint ENABLE_INSERT_MODE = 32;
const uint ENABLE_QUICK_EDIT_MODE = 64;
const uint ENABLE_EXTENDED_FLAGS = 128;
const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
// Output modes.
const uint ENABLE_PROCESSED_OUTPUT = 1;
const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
[DllImport ("kernel32.dll", SetLastError = true)]
static extern IntPtr GetStdHandle (int nStdHandle);
[DllImport ("kernel32.dll")]
static extern bool GetConsoleMode (IntPtr hConsoleHandle, out uint lpMode);
[DllImport ("kernel32.dll")]
static extern bool SetConsoleMode (IntPtr hConsoleHandle, uint dwMode);
[DllImport ("kernel32.dll")]
static extern uint GetLastError ();
}
internal class NetEvents {
ManualResetEventSlim inputReady = new ManualResetEventSlim (false);
ManualResetEventSlim waitForStart = new ManualResetEventSlim (false);
ManualResetEventSlim winChange = new ManualResetEventSlim (false);
Queue<InputResult?> inputResultQueue = new Queue<InputResult?> ();
ConsoleDriver consoleDriver;
int lastWindowHeight;
int largestWindowHeight;
#if PROCESS_REQUEST
bool neededProcessRequest;
#endif
public int NumberOfCSI { get; }
public NetEvents (ConsoleDriver consoleDriver, int numberOfCSI = 1)
{
if (consoleDriver == null) {
throw new ArgumentNullException ("Console driver instance must be provided.");
}
this.consoleDriver = consoleDriver;
NumberOfCSI = numberOfCSI;
Task.Run (ProcessInputResultQueue);
Task.Run (CheckWinChange);
}
public InputResult? ReadConsoleInput ()
{
while (true) {
waitForStart.Set ();
winChange.Set ();
if (inputResultQueue.Count == 0) {
inputReady.Wait ();
inputReady.Reset ();
}
#if PROCESS_REQUEST
neededProcessRequest = false;
#endif
if (inputResultQueue.Count > 0) {
return inputResultQueue.Dequeue ();
}
}
}
void ProcessInputResultQueue ()
{
while (true) {
waitForStart.Wait ();
waitForStart.Reset ();
if (inputResultQueue.Count == 0) {
GetConsoleInputType (Console.ReadKey (true));
}
inputReady.Set ();
}
}
void CheckWinChange ()
{
while (true) {
winChange.Wait ();
winChange.Reset ();
WaitWinChange ();
inputReady.Set ();
}
}
void WaitWinChange ()
{
while (true) {
// HACK: Sleep for 10ms to mitigate high CPU usage (see issue #1502). 10ms was tested to address the problem, but may not be correct.
Thread.Sleep (10);
if (!consoleDriver.HeightAsBuffer) {
if (Console.WindowWidth != consoleDriver.Cols || Console.WindowHeight != consoleDriver.Rows) {
var w = Math.Max (Console.WindowWidth, 0);
var h = Math.Max (Console.WindowHeight, 0);
GetWindowSizeEvent (new Size (w, h));
return;
}
} else {
//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
largestWindowHeight = Console.BufferHeight;
if (Console.BufferWidth != consoleDriver.Cols || largestWindowHeight != consoleDriver.Rows
|| Console.WindowHeight != lastWindowHeight) {
lastWindowHeight = Console.WindowHeight;
GetWindowSizeEvent (new Size (Console.BufferWidth, lastWindowHeight));
return;
}
if (Console.WindowTop != consoleDriver.Top) {
// Top only working on Windows.
var winPositionEv = new WindowPositionEvent () {
Top = Console.WindowTop,
Left = Console.WindowLeft
};
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.WindowPosition,
WindowPositionEvent = winPositionEv
});
return;
}
#if PROCESS_REQUEST
if (!neededProcessRequest) {
Console.Out.Write ("\x1b[6n");
neededProcessRequest = true;
}
#endif
}
}
}
void GetWindowSizeEvent (Size size)
{
WindowSizeEvent windowSizeEvent = new WindowSizeEvent () {
Size = size
};
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.WindowSize,
WindowSizeEvent = windowSizeEvent
});
}
void GetConsoleInputType (ConsoleKeyInfo consoleKeyInfo)
{
InputResult inputResult = new InputResult {
EventType = EventType.Key
};
ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
ConsoleKey key = 0;
MouseEvent mouseEvent = new MouseEvent ();
var keyChar = consoleKeyInfo.KeyChar;
switch ((uint)keyChar) {
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);
}
break;
case uint n when (n >= '\u0001' && n <= '\u001a'):
if (consoleKeyInfo.Key == 0 && consoleKeyInfo.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);
} 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);
}
break;
case 27:
//case 91:
ConsoleKeyInfo [] cki = new ConsoleKeyInfo [] { consoleKeyInfo };
ConsoleModifiers mod = consoleKeyInfo.Modifiers;
int delay = 0;
while (delay < 100) {
if (Console.KeyAvailable) {
do {
var result = Console.ReadKey (true);
Array.Resize (ref cki, cki == null ? 1 : cki.Length + 1);
cki [cki.Length - 1] = result;
} while (Console.KeyAvailable);
break;
}
Thread.Sleep (50);
delay += 50;
}
SplitCSI (cki, ref inputResult, ref newConsoleKeyInfo, ref key, ref mouseEvent, ref mod);
return;
case 127:
newConsoleKeyInfo = new ConsoleKeyInfo (consoleKeyInfo.KeyChar, ConsoleKey.Backspace,
(consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
(consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
(consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0);
break;
default:
newConsoleKeyInfo = consoleKeyInfo;
break;
}
if (inputResult.EventType == EventType.Key) {
inputResult.ConsoleKeyInfo = newConsoleKeyInfo;
} else {
inputResult.MouseEvent = mouseEvent;
}
inputResultQueue.Enqueue (inputResult);
}
void SplitCSI (ConsoleKeyInfo [] cki, ref InputResult inputResult, ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ref MouseEvent mouseEvent, ref ConsoleModifiers mod)
{
ConsoleKeyInfo [] splitedCki = new ConsoleKeyInfo [] { };
int length = 0;
var kChar = GetKeyCharArray (cki);
var nCSI = GetNumberOfCSI (kChar);
int curCSI = 0;
char previousKChar = '\0';
if (nCSI > 1) {
for (int i = 0; i < cki.Length; i++) {
var ck = cki [i];
if (NumberOfCSI > 0 && nCSI - curCSI > NumberOfCSI) {
if (i + 1 < cki.Length && cki [i + 1].KeyChar == '\x1b' && previousKChar != '\0') {
curCSI++;
previousKChar = '\0';
} else {
previousKChar = ck.KeyChar;
}
continue;
}
if (ck.KeyChar == '\x1b') {
if (ck.KeyChar == 'R') {
ResizeArray (ck);
}
if (splitedCki.Length > 1) {
DecodeCSI (ref inputResult, ref newConsoleKeyInfo, ref key, ref mouseEvent, splitedCki, ref mod);
}
splitedCki = new ConsoleKeyInfo [] { };
length = 0;
}
ResizeArray (ck);
if (i == cki.Length - 1 && splitedCki.Length > 0) {
DecodeCSI (ref inputResult, ref newConsoleKeyInfo, ref key, ref mouseEvent, splitedCki, ref mod);
}
}
} else {
DecodeCSI (ref inputResult, ref newConsoleKeyInfo, ref key, ref mouseEvent, cki, ref mod);
}
void ResizeArray (ConsoleKeyInfo ck)
{
length++;
Array.Resize (ref splitedCki, length);
splitedCki [length - 1] = ck;
}
}
char [] GetKeyCharArray (ConsoleKeyInfo [] cki)
{
char [] kChar = new char [] { };
var length = 0;
foreach (var kc in cki) {
length++;
Array.Resize (ref kChar, length);
kChar [length - 1] = kc.KeyChar;
}
return kChar;
}
int GetNumberOfCSI (char [] csi)
{
int nCSI = 0;
for (int i = 0; i < csi.Length; i++) {
if (csi [i] == '\x1b' || (csi [i] == '[' && (i == 0 || (i > 0 && csi [i - 1] != '\x1b')))) {
nCSI++;
}
}
return nCSI;
}
void DecodeCSI (ref InputResult inputResult, ref ConsoleKeyInfo newConsoleKeyInfo, ref ConsoleKey key, ref MouseEvent mouseEvent, ConsoleKeyInfo [] cki, ref ConsoleModifiers mod)
{
switch (cki.Length) {
case 2:
if ((uint)cki [1].KeyChar >= 1 && (uint)cki [1].KeyChar <= 26) {
key = (ConsoleKey)(char)(cki [1].KeyChar + (uint)ConsoleKey.A - 1);
newConsoleKeyInfo = new ConsoleKeyInfo (cki [1].KeyChar,
key,
false,
true,
true);
} else {
if (cki [1].KeyChar >= 97 && cki [1].KeyChar <= 122) {
key = (ConsoleKey)cki [1].KeyChar.ToString ().ToUpper () [0];
} else {
key = (ConsoleKey)cki [1].KeyChar;
}
newConsoleKeyInfo = new ConsoleKeyInfo ((char)key,
(ConsoleKey)Math.Min ((uint)key, 255),
false,
true,
false);
}
break;
case 3:
if (cki [1].KeyChar == '[' || cki [1].KeyChar == 79) {
key = GetConsoleKey (cki [2].KeyChar, ref mod, cki.Length);
}
newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
(mod & ConsoleModifiers.Control) != 0);
break;
case 4:
if (cki [1].KeyChar == '[' && cki [3].KeyChar == 126) {
key = GetConsoleKey (cki [2].KeyChar, ref mod, cki.Length);
newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
(mod & ConsoleModifiers.Control) != 0);
}
break;
case 5:
if (cki [1].KeyChar == '[' && (cki [2].KeyChar == 49 || cki [2].KeyChar == 50)
&& cki [4].KeyChar == 126) {
key = GetConsoleKey (cki [3].KeyChar, ref mod, cki.Length);
} else if (cki [1].KeyChar == 49 && cki [2].KeyChar == ';') { // For WSL
mod |= GetConsoleModifiers (cki [3].KeyChar);
key = ConsoleKey.End;
}
newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
(mod & ConsoleModifiers.Control) != 0);
break;
case 6:
if (cki [1].KeyChar == '[' && cki [2].KeyChar == 49 && cki [3].KeyChar == ';') {
mod |= GetConsoleModifiers (cki [4].KeyChar);
key = GetConsoleKey (cki [5].KeyChar, ref mod, cki.Length);
} else if (cki [1].KeyChar == '[' && cki [3].KeyChar == ';') {
mod |= GetConsoleModifiers (cki [4].KeyChar);
key = GetConsoleKey (cki [2].KeyChar, ref mod, cki.Length);
}
newConsoleKeyInfo = new ConsoleKeyInfo ('\0',
key,
(mod & ConsoleModifiers.Shift) != 0,
(mod & ConsoleModifiers.Alt) != 0,
(mod & ConsoleModifiers.Control) != 0);
break;
case 7:
GetRequestEvent (GetKeyCharArray (cki));
return;
case int n when n >= 8:
GetMouseEvent (cki);
return;
}
if (inputResult.EventType == EventType.Key) {
inputResult.ConsoleKeyInfo = newConsoleKeyInfo;
} else {
inputResult.MouseEvent = mouseEvent;
}
inputResultQueue.Enqueue (inputResult);
}
Point lastCursorPosition;
void GetRequestEvent (char [] kChar)
{
EventType eventType = new EventType ();
Point point = new Point ();
int foundPoint = 0;
string value = "";
for (int i = 0; i < kChar.Length; i++) {
var c = kChar [i];
if (c == '\u001b' || c == '[') {
foundPoint++;
} else if (foundPoint == 1 && c != ';' && c != '?') {
value += c.ToString ();
} else if (c == '?') {
foundPoint++;
} else if (c == ';') {
if (foundPoint >= 1) {
point.Y = int.Parse (value) - 1;
}
value = "";
foundPoint++;
} else if (foundPoint > 0 && i < kChar.Length - 1) {
value += c.ToString ();
} else if (i == kChar.Length - 1) {
point.X = int.Parse (value) + Console.WindowTop - 1;
switch (c) {
case 'R':
if (lastCursorPosition.Y != point.Y) {
lastCursorPosition = point;
eventType = EventType.WindowPosition;
var winPositionEv = new WindowPositionEvent () {
CursorPosition = point
};
inputResultQueue.Enqueue (new InputResult () {
EventType = eventType,
WindowPositionEvent = winPositionEv
});
} else {
return;
}
break;
case 'c': // CSI?1;0c ("VT101 with No Options")
break;
default:
throw new NotImplementedException ();
}
}
}
inputReady.Set ();
}
MouseEvent lastMouseEvent;
bool isButtonPressed;
bool isButtonClicked;
bool isButtonDoubleClicked;
bool isButtonTripleClicked;
bool isProcContBtnPressedRuning;
int buttonPressedCount;
//bool isButtonReleased;
void GetMouseEvent (ConsoleKeyInfo [] cki)
{
MouseEvent mouseEvent = new MouseEvent ();
MouseButtonState buttonState = 0;
Point point = new Point ();
int buttonCode = 0;
bool foundButtonCode = false;
int foundPoint = 0;
string value = "";
var kChar = GetKeyCharArray (cki);
//System.Diagnostics.Debug.WriteLine ($"kChar: {new string (kChar)}");
for (int i = 0; i < kChar.Length; i++) {
var c = kChar [i];
if (c == '<') {
foundButtonCode = true;
} else if (foundButtonCode && c != ';') {
value += c.ToString ();
} else if (c == ';') {
if (foundButtonCode) {
foundButtonCode = false;
buttonCode = int.Parse (value);
}
if (foundPoint == 1) {
point.X = int.Parse (value) - 1;
}
value = "";
foundPoint++;
} else if (foundPoint > 0 && c != 'm' && c != 'M') {
value += c.ToString ();
} else if (c == 'm' || c == 'M') {
point.Y = int.Parse (value) + Console.WindowTop - 1;
//if (c == 'M') {
// isButtonPressed = true;
//} else if (c == 'm') {
// isButtonPressed = false;
//}
//System.Diagnostics.Debug.WriteLine ($"buttonCode: {buttonCode}");
switch (buttonCode) {
case 0:
case 8:
case 16:
case 24:
case 32:
case 36:
case 40:
case 48:
case 56:
buttonState = c == 'M' ? MouseButtonState.Button1Pressed
: MouseButtonState.Button1Released;
break;
case 1:
case 9:
case 17:
case 25:
case 33:
case 37:
case 41:
case 45:
case 49:
case 53:
case 57:
case 61:
buttonState = c == 'M' ? MouseButtonState.Button2Pressed
: MouseButtonState.Button2Released;
break;
case 2:
case 10:
case 14:
case 18:
case 22:
case 26:
case 30:
case 34:
case 42:
case 46:
case 50:
case 54:
case 58:
case 62:
buttonState = c == 'M' ? MouseButtonState.Button3Pressed
: MouseButtonState.Button3Released;
break;
case 35:
case 39:
case 43:
case 47:
case 55:
case 59:
case 63:
buttonState = MouseButtonState.ReportMousePosition;
break;
case 64:
buttonState = MouseButtonState.ButtonWheeledUp;
break;
case 65:
buttonState = MouseButtonState.ButtonWheeledDown;
break;
case 68:
case 72:
case 80:
buttonState = MouseButtonState.ButtonWheeledLeft; // Shift/Ctrl+ButtonWheeledUp
break;
case 69:
case 73:
case 81:
buttonState = MouseButtonState.ButtonWheeledRight; // Shift/Ctrl+ButtonWheeledDown
break;
}
// Modifiers.
switch (buttonCode) {
case 8:
case 9:
case 10:
case 43:
buttonState |= MouseButtonState.ButtonAlt;
break;
case 14:
case 47:
buttonState |= MouseButtonState.ButtonAlt | MouseButtonState.ButtonShift;
break;
case 16:
case 17:
case 18:
case 51:
buttonState |= MouseButtonState.ButtonCtrl;
break;
case 22:
case 55:
buttonState |= MouseButtonState.ButtonCtrl | MouseButtonState.ButtonShift;
break;
case 24:
case 25:
case 26:
case 59:
buttonState |= MouseButtonState.ButtonAlt | MouseButtonState.ButtonCtrl;
break;
case 30:
case 63:
buttonState |= MouseButtonState.ButtonCtrl | MouseButtonState.ButtonShift | MouseButtonState.ButtonAlt;
break;
case 32:
case 33:
case 34:
buttonState |= MouseButtonState.ReportMousePosition;
break;
case 36:
case 37:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonShift;
break;
case 39:
case 68:
case 69:
buttonState |= MouseButtonState.ButtonShift;
break;
case 40:
case 41:
case 42:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonAlt;
break;
case 45:
case 46:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonAlt | MouseButtonState.ButtonShift;
break;
case 48:
case 49:
case 50:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonCtrl;
break;
case 53:
case 54:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonCtrl | MouseButtonState.ButtonShift;
break;
case 56:
case 57:
case 58:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonCtrl | MouseButtonState.ButtonAlt;
break;
case 61:
case 62:
buttonState |= MouseButtonState.ReportMousePosition | MouseButtonState.ButtonCtrl | MouseButtonState.ButtonShift | MouseButtonState.ButtonAlt;
break;
}
}
}
mouseEvent.Position.X = point.X;
mouseEvent.Position.Y = point.Y;
mouseEvent.ButtonState = buttonState;
//System.Diagnostics.Debug.WriteLine ($"ButtonState: {mouseEvent.ButtonState} X: {mouseEvent.Position.X} Y: {mouseEvent.Position.Y}");
if (isButtonDoubleClicked) {
Application.MainLoop.AddIdle (() => {
Task.Run (async () => await ProcessButtonDoubleClickedAsync ());
return false;
});
}
if ((buttonState & MouseButtonState.Button1Pressed) != 0
|| (buttonState & MouseButtonState.Button2Pressed) != 0
|| (buttonState & MouseButtonState.Button3Pressed) != 0) {
if ((buttonState & MouseButtonState.ReportMousePosition) == 0) {
buttonPressedCount++;
} else {
buttonPressedCount = 0;
}
//System.Diagnostics.Debug.WriteLine ($"buttonPressedCount: {buttonPressedCount}");
isButtonPressed = true;
} else {
isButtonPressed = false;
buttonPressedCount = 0;
}
if (buttonPressedCount == 2 && !isButtonDoubleClicked
&& (lastMouseEvent.ButtonState == MouseButtonState.Button1Pressed
|| lastMouseEvent.ButtonState == MouseButtonState.Button2Pressed
|| lastMouseEvent.ButtonState == MouseButtonState.Button3Pressed)) {
isButtonDoubleClicked = true;
ProcessButtonDoubleClicked (mouseEvent);
inputReady.Set ();
return;
} else if (buttonPressedCount == 3 && isButtonDoubleClicked
&& (lastMouseEvent.ButtonState == MouseButtonState.Button1Pressed
|| lastMouseEvent.ButtonState == MouseButtonState.Button2Pressed
|| lastMouseEvent.ButtonState == MouseButtonState.Button3Pressed)) {
isButtonDoubleClicked = false;
isButtonTripleClicked = true;
buttonPressedCount = 0;
ProcessButtonTripleClicked (mouseEvent);
lastMouseEvent = mouseEvent;
inputReady.Set ();
return;
}
//System.Diagnostics.Debug.WriteLine ($"isButtonClicked: {isButtonClicked} isButtonDoubleClicked: {isButtonDoubleClicked} isButtonTripleClicked: {isButtonTripleClicked}");
if ((isButtonClicked || isButtonDoubleClicked || isButtonTripleClicked)
&& ((buttonState & MouseButtonState.Button1Released) != 0
|| (buttonState & MouseButtonState.Button2Released) != 0
|| (buttonState & MouseButtonState.Button3Released) != 0)) {
//isButtonClicked = false;
//isButtonDoubleClicked = false;
isButtonTripleClicked = false;
buttonPressedCount = 0;
return;
}
if (isButtonClicked && !isButtonDoubleClicked && lastMouseEvent.Position != default && lastMouseEvent.Position == point
&& ((buttonState & MouseButtonState.Button1Pressed) != 0
|| (buttonState & MouseButtonState.Button2Pressed) != 0
|| (buttonState & MouseButtonState.Button3Pressed) != 0
|| (buttonState & MouseButtonState.Button1Released) != 0
|| (buttonState & MouseButtonState.Button2Released) != 0
|| (buttonState & MouseButtonState.Button3Released) != 0)) {
isButtonClicked = false;
isButtonDoubleClicked = true;
ProcessButtonDoubleClicked (mouseEvent);
Application.MainLoop.AddIdle (() => {
Task.Run (async () => {
await Task.Delay (600);
isButtonDoubleClicked = false;
});
return false;
});
inputReady.Set ();
return;
}
if (isButtonDoubleClicked && lastMouseEvent.Position != default && lastMouseEvent.Position == point
&& ((buttonState & MouseButtonState.Button1Pressed) != 0
|| (buttonState & MouseButtonState.Button2Pressed) != 0
|| (buttonState & MouseButtonState.Button3Pressed) != 0
|| (buttonState & MouseButtonState.Button1Released) != 0
|| (buttonState & MouseButtonState.Button2Released) != 0
|| (buttonState & MouseButtonState.Button3Released) != 0)) {
isButtonDoubleClicked = false;
isButtonTripleClicked = true;
ProcessButtonTripleClicked (mouseEvent);
inputReady.Set ();
return;
}
//if (!isButtonPressed && !isButtonClicked && !isButtonDoubleClicked && !isButtonTripleClicked
// && !isButtonReleased && lastMouseEvent.ButtonState != 0
// && ((buttonState & MouseButtonState.Button1Released) == 0
// && (buttonState & MouseButtonState.Button2Released) == 0
// && (buttonState & MouseButtonState.Button3Released) == 0)) {
// ProcessButtonReleased (lastMouseEvent);
// inputReady.Set ();
// return;
//}
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.Mouse,
MouseEvent = mouseEvent
});
if (!isButtonClicked && !lastMouseEvent.ButtonState.HasFlag (MouseButtonState.ReportMousePosition)
&& lastMouseEvent.Position != default && lastMouseEvent.Position == point
&& ((buttonState & MouseButtonState.Button1Released) != 0
|| (buttonState & MouseButtonState.Button2Released) != 0
|| (buttonState & MouseButtonState.Button3Released) != 0)) {
isButtonClicked = true;
ProcessButtonClicked (mouseEvent);
Application.MainLoop.AddIdle (() => {
Task.Run (async () => {
await Task.Delay (300);
isButtonClicked = false;
});
return false;
});
inputReady.Set ();
return;
}
lastMouseEvent = mouseEvent;
if (isButtonPressed && !isButtonClicked && !isButtonDoubleClicked && !isButtonTripleClicked && !isProcContBtnPressedRuning) {
//isButtonReleased = false;
if ((buttonState & MouseButtonState.ReportMousePosition) != 0) {
point = new Point ();
} else {
point = new Point () {
X = mouseEvent.Position.X,
Y = mouseEvent.Position.Y
};
}
if ((buttonState & MouseButtonState.ReportMousePosition) == 0) {
Application.MainLoop.AddIdle (() => {
Task.Run (async () => await ProcessContinuousButtonPressedAsync ());
return false;
});
}
}
inputReady.Set ();
}
void ProcessButtonClicked (MouseEvent mouseEvent)
{
var me = new MouseEvent () {
Position = mouseEvent.Position,
ButtonState = mouseEvent.ButtonState
};
if ((mouseEvent.ButtonState & MouseButtonState.Button1Released) != 0) {
me.ButtonState &= ~MouseButtonState.Button1Released;
me.ButtonState |= MouseButtonState.Button1Clicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button2Released) != 0) {
me.ButtonState &= ~MouseButtonState.Button2Released;
me.ButtonState |= MouseButtonState.Button2Clicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button3Released) != 0) {
me.ButtonState &= ~MouseButtonState.Button3Released;
me.ButtonState |= MouseButtonState.Button3Clicked;
}
//isButtonReleased = true;
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.Mouse,
MouseEvent = me
});
}
async Task ProcessButtonDoubleClickedAsync ()
{
await Task.Delay (300);
isButtonDoubleClicked = false;
buttonPressedCount = 0;
}
void ProcessButtonDoubleClicked (MouseEvent mouseEvent)
{
var me = new MouseEvent () {
Position = mouseEvent.Position,
ButtonState = mouseEvent.ButtonState
};
if ((mouseEvent.ButtonState & MouseButtonState.Button1Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button1Pressed;
me.ButtonState |= MouseButtonState.Button1DoubleClicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button2Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button2Pressed;
me.ButtonState |= MouseButtonState.Button2DoubleClicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button3Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button3Pressed;
me.ButtonState |= MouseButtonState.Button3DoubleClicked;
}
//isButtonReleased = true;
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.Mouse,
MouseEvent = me
});
}
void ProcessButtonTripleClicked (MouseEvent mouseEvent)
{
var me = new MouseEvent () {
Position = mouseEvent.Position,
ButtonState = mouseEvent.ButtonState
};
if ((mouseEvent.ButtonState & MouseButtonState.Button1Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button1Pressed;
me.ButtonState |= MouseButtonState.Button1TripleClicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button2Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button2Pressed;
me.ButtonState |= MouseButtonState.Button2TrippleClicked;
} else if ((mouseEvent.ButtonState & MouseButtonState.Button3Pressed) != 0) {
me.ButtonState &= ~MouseButtonState.Button3Pressed;
me.ButtonState |= MouseButtonState.Button3TripleClicked;
}
//isButtonReleased = true;
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.Mouse,
MouseEvent = me
});
}
async Task ProcessContinuousButtonPressedAsync ()
{
isProcContBtnPressedRuning = true;
await Task.Delay (200);
while (isButtonPressed) {
await Task.Delay (100);
var view = Application.WantContinuousButtonPressedView;
if (view == null) {
break;
}
if (isButtonPressed && (lastMouseEvent.ButtonState & MouseButtonState.ReportMousePosition) == 0) {
inputResultQueue.Enqueue (new InputResult () {
EventType = EventType.Mouse,
MouseEvent = lastMouseEvent
});
inputReady.Set ();
}
}
isProcContBtnPressedRuning = false;
//isButtonPressed = false;
}
//void ProcessButtonReleased (MouseEvent mouseEvent)
//{
// var me = new MouseEvent () {
// Position = mouseEvent.Position,
// ButtonState = mouseEvent.ButtonState
// };
// if ((mouseEvent.ButtonState & MouseButtonState.Button1Pressed) != 0) {
// me.ButtonState &= ~(MouseButtonState.Button1Pressed | MouseButtonState.ReportMousePosition);
// me.ButtonState |= MouseButtonState.Button1Released;
// } else if ((mouseEvent.ButtonState & MouseButtonState.Button2Pressed) != 0) {
// me.ButtonState &= ~(MouseButtonState.Button2Pressed | MouseButtonState.ReportMousePosition);
// me.ButtonState |= MouseButtonState.Button2Released;
// } else if ((mouseEvent.ButtonState & MouseButtonState.Button3Pressed) != 0) {
// me.ButtonState &= ~(MouseButtonState.Button3Pressed | MouseButtonState.ReportMousePosition);
// me.ButtonState |= MouseButtonState.Button3Released;
// }
// isButtonReleased = true;
// lastMouseEvent = me;
// inputResultQueue.Enqueue (new InputResult () {
// EventType = EventType.Mouse,
// MouseEvent = me
// });
//}
ConsoleModifiers GetConsoleModifiers (uint keyChar)
{
switch (keyChar) {
case 50:
return ConsoleModifiers.Shift;
case 51:
return ConsoleModifiers.Alt;
case 52:
return ConsoleModifiers.Shift | ConsoleModifiers.Alt;
case 53:
return ConsoleModifiers.Control;
case 54:
return ConsoleModifiers.Shift | ConsoleModifiers.Control;
case 55:
return ConsoleModifiers.Alt | ConsoleModifiers.Control;
case 56:
return ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control;
default:
return 0;
}
}
ConsoleKey GetConsoleKey (char keyChar, ref ConsoleModifiers mod, int length)
{
ConsoleKey key;
switch (keyChar) {
case 'A':
key = ConsoleKey.UpArrow;
break;
case 'B':
key = ConsoleKey.DownArrow;
break;
case 'C':
key = ConsoleKey.RightArrow;
break;
case 'D':
key = ConsoleKey.LeftArrow;
break;
case 'F':
key = ConsoleKey.End;
break;
case 'H':
key = ConsoleKey.Home;
break;
case 'P':
key = ConsoleKey.F1;
break;
case 'Q':
key = ConsoleKey.F2;
break;
case 'R':
key = ConsoleKey.F3;
break;
case 'S':
key = ConsoleKey.F4;
break;
case 'Z':
key = ConsoleKey.Tab;
mod |= ConsoleModifiers.Shift;
break;
case '0':
key = ConsoleKey.F9;
break;
case '1':
key = ConsoleKey.F10;
break;
case '2':
key = ConsoleKey.Insert;
break;
case '3':
if (length == 5) {
key = ConsoleKey.F11;
} else {
key = ConsoleKey.Delete;
}
break;
case '4':
key = ConsoleKey.F12;
break;
case '5':
if (length == 5) {
key = ConsoleKey.F5;
} else {
key = ConsoleKey.PageUp;
}
break;
case '6':
key = ConsoleKey.PageDown;
break;
case '7':
key = ConsoleKey.F6;
break;
case '8':
key = ConsoleKey.F7;
break;
case '9':
key = ConsoleKey.F8;
break;
default:
key = 0;
break;
}
return key;
}
public enum EventType {
Key = 1,
Mouse = 2,
WindowSize = 3,
WindowPosition = 4
}
[Flags]
public enum MouseButtonState {
Button1Pressed = 0x1,
Button1Released = 0x2,
Button1Clicked = 0x4,
Button1DoubleClicked = 0x8,
Button1TripleClicked = 0x10,
Button2Pressed = 0x20,
Button2Released = 0x40,
Button2Clicked = 0x80,
Button2DoubleClicked = 0x100,
Button2TrippleClicked = 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 = Button1Pressed | Button1Released | Button1Clicked | Button1DoubleClicked | Button1TripleClicked | Button2Pressed | Button2Released | Button2Clicked | Button2DoubleClicked | Button2TrippleClicked | Button3Pressed | Button3Released | Button3Clicked | Button3DoubleClicked | Button3TripleClicked | ButtonWheeledUp | ButtonWheeledDown | ButtonWheeledLeft | ButtonWheeledRight | Button4Pressed | Button4Released | Button4Clicked | Button4DoubleClicked | Button4TripleClicked | ReportMousePosition
}
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 InputResult {
public EventType EventType;
public ConsoleKeyInfo ConsoleKeyInfo;
public MouseEvent MouseEvent;
public WindowSizeEvent WindowSizeEvent;
public WindowPositionEvent WindowPositionEvent;
}
}
internal class NetDriver : ConsoleDriver {
const int COLOR_BLACK = 30;
const int COLOR_RED = 31;
const int COLOR_GREEN = 32;
const int COLOR_YELLOW = 33;
const int COLOR_BLUE = 34;
const int COLOR_MAGENTA = 35;
const int COLOR_CYAN = 36;
const int COLOR_WHITE = 37;
const int COLOR_BRIGHT_BLACK = 90;
const int COLOR_BRIGHT_RED = 91;
const int COLOR_BRIGHT_GREEN = 92;
const int COLOR_BRIGHT_YELLOW = 93;
const int COLOR_BRIGHT_BLUE = 94;
const int COLOR_BRIGHT_MAGENTA = 95;
const int COLOR_BRIGHT_CYAN = 96;
const int COLOR_BRIGHT_WHITE = 97;
int cols, rows, left, top;
public override int Cols => cols;
public override int Rows => rows;
public override int Left => left;
public override int Top => top;
public override bool HeightAsBuffer { get; set; }
public NetWinVTConsole NetWinConsole { get; }
public bool IsWinPlatform { get; }
public override IClipboard Clipboard { get; }
public override int [,,] Contents => contents;
int largestWindowHeight;
public NetDriver ()
{
var p = Environment.OSVersion.Platform;
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) {
IsWinPlatform = true;
NetWinConsole = new NetWinVTConsole ();
}
//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
largestWindowHeight = Console.BufferHeight;
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 ();
}
}
}
// The format is rows, columns and 3 values on the last column: Rune, Attribute and Dirty Flag
int [,,] contents;
bool [] dirtyLine;
static bool sync = false;
// Current row, and current col, tracked by Move/AddCh only
int ccol, crow;
public override void Move (int col, int row)
{
ccol = col;
crow = row;
}
public override void AddRune (Rune rune)
{
if (contents.Length != Rows * Cols * 3) {
return;
}
rune = MakePrintable (rune);
var runeWidth = Rune.ColumnWidth (rune);
var validClip = IsValidContent (ccol, crow, Clip);
if (validClip) {
if (runeWidth < 2 && ccol > 0
&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
contents [crow, ccol - 1, 0] = (int)(uint)' ';
} else if (runeWidth < 2 && ccol <= Clip.Right - 1
&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
contents [crow, ccol + 1, 0] = (int)(uint)' ';
contents [crow, ccol + 1, 2] = 1;
}
if (runeWidth > 1 && ccol == Clip.Right - 1) {
contents [crow, ccol, 0] = (int)(uint)' ';
} else {
contents [crow, ccol, 0] = (int)(uint)rune;
}
contents [crow, ccol, 1] = CurrentAttribute;
contents [crow, ccol, 2] = 1;
dirtyLine [crow] = true;
}
ccol++;
if (runeWidth > 1) {
if (validClip && ccol < Clip.Right) {
contents [crow, ccol, 1] = CurrentAttribute;
contents [crow, ccol, 2] = 0;
}
ccol++;
}
//if (ccol == Cols) {
// ccol = 0;
// if (crow + 1 < Rows)
// crow++;
//}
if (sync) {
UpdateScreen ();
}
}
public override void AddStr (ustring str)
{
foreach (var rune in str)
AddRune (rune);
}
public override void End ()
{
if (IsWinPlatform) {
NetWinConsole.Cleanup ();
}
StopReportingMouseMoves ();
Console.ResetColor ();
Clear ();
//Set cursor key to cursor.
Console.Out.Write ("\x1b[?25h");
Console.Out.Flush ();
}
void Clear ()
{
if (Rows > 0) {
Console.Clear ();
Console.Out.Write ("\x1b[3J");
//Console.Out.Write ("\x1b[?25l");
}
}
public override Attribute MakeColor (Color foreground, Color background)
{
return MakeColor ((ConsoleColor)foreground, (ConsoleColor)background);
}
static Attribute MakeColor (ConsoleColor f, ConsoleColor b)
{
// Encode the colors into the int value.
return new Attribute (
value: ((((int)f) & 0xffff) << 16) | (((int)b) & 0xffff),
foreground: (Color)f,
background: (Color)b
);
}
public override void Init (Action terminalResized)
{
TerminalResized = terminalResized;
//Set cursor key to application.
Console.Out.Write ("\x1b[?25l");
Console.Out.Flush ();
Console.TreatControlCAsInput = true;
cols = Console.WindowWidth;
rows = Console.WindowHeight;
CurrentAttribute = MakeColor (Color.White, Color.Black);
InitalizeColorSchemes ();
ResizeScreen ();
UpdateOffScreen ();
StartReportingMouseMoves ();
Clear ();
}
public override void ResizeScreen ()
{
if (!HeightAsBuffer) {
if (Console.WindowHeight > 0) {
// Can raise an exception while is still resizing.
try {
// Not supported on Unix.
if (IsWinPlatform) {
#pragma warning disable CA1416
Console.CursorTop = 0;
Console.CursorLeft = 0;
Console.WindowTop = 0;
Console.WindowLeft = 0;
Console.SetBufferSize (Cols, Rows);
#pragma warning restore CA1416
} else {
//Console.Out.Write ($"\x1b[8;{Console.WindowHeight};{Console.WindowWidth}t");
Console.Out.Write ($"\x1b[0;0" +
$";{Rows};{Cols}w");
}
} catch (System.IO.IOException) {
return;
} catch (ArgumentOutOfRangeException) {
return;
}
}
} else {
if (IsWinPlatform && Console.WindowHeight > 0) {
// Can raise an exception while is still resizing.
try {
#pragma warning disable CA1416
Console.WindowTop = Math.Max (Math.Min (top, Rows - Console.WindowHeight), 0);
#pragma warning restore CA1416
} catch (Exception) {
return;
}
} else {
Console.Out.Write ($"\x1b[{top};{Console.WindowLeft}" +
$";{Rows};{Cols}w");
}
}
Clip = new Rect (0, 0, Cols, Rows);
Console.Out.Write ("\x1b[3J");
Console.Out.Flush ();
}
public override void UpdateOffScreen ()
{
contents = new int [Rows, Cols, 3];
dirtyLine = new bool [Rows];
lock (contents) {
// Can raise an exception while is still resizing.
try {
for (int row = 0; row < rows; row++) {
for (int c = 0; c < cols; c++) {
contents [row, c, 0] = ' ';
contents [row, c, 1] = (ushort)Colors.TopLevel.Normal;
contents [row, c, 2] = 0;
dirtyLine [row] = true;
}
}
} catch (IndexOutOfRangeException) { }
}
}
public override Attribute MakeAttribute (Color fore, Color back)
{
return MakeColor ((ConsoleColor)fore, (ConsoleColor)back);
}
public override void Refresh ()
{
UpdateScreen ();
UpdateCursor ();
}
int redrawAttr = -1;
public override void UpdateScreen ()
{
if (Console.WindowHeight == 0 || contents.Length != Rows * Cols * 3
|| (!HeightAsBuffer && Rows != Console.WindowHeight)
|| (HeightAsBuffer && Rows != largestWindowHeight)) {
return;
}
int top = Top;
int left = Left;
int rows = Math.Min (Console.WindowHeight + top, Rows);
int cols = Cols;
System.Text.StringBuilder output = new System.Text.StringBuilder ();
var lastCol = -1;
Console.CursorVisible = false;
for (int row = top; row < rows; row++) {
if (!dirtyLine [row]) {
continue;
}
dirtyLine [row] = false;
output.Clear ();
for (int col = left; col < cols; col++) {
if (Console.WindowHeight > 0 && !SetCursorPosition (col, row)) {
return;
}
lastCol = -1;
var outputWidth = 0;
for (; col < cols; col++) {
if (contents [row, col, 2] == 0) {
if (output.Length > 0) {
//Console.CursorLeft = lastCol;
//Console.CursorTop = row;
SetVirtualCursorPosition (lastCol, row);
Console.Write (output);
output.Clear ();
lastCol += outputWidth;
outputWidth = 0;
} else if (lastCol == -1) {
lastCol = col;
}
if (lastCol + 1 < cols)
lastCol++;
continue;
}
if (lastCol == -1)
lastCol = col;
var attr = contents [row, col, 1];
if (attr != redrawAttr) {
output.Append (WriteAttributes (attr));
}
outputWidth++;
var rune = contents [row, col, 0];
char [] spair;
if (Rune.DecodeSurrogatePair((uint) rune, out spair)) {
output.Append (spair);
} else {
output.Append ((char)rune);
}
contents [row, col, 2] = 0;
}
}
if (output.Length > 0) {
//Console.CursorLeft = lastCol;
//Console.CursorTop = row;
SetVirtualCursorPosition (lastCol, row);
Console.Write (output);
}
}
}
void SetVirtualCursorPosition (int lastCol, int row)
{
Console.Out.Write ($"\x1b[{row + 1};{lastCol + 1}H");
Console.Out.Flush ();
}
System.Text.StringBuilder WriteAttributes (int attr)
{
const string CSI = "\x1b[";
int bg = 0;
int fg = 0;
System.Text.StringBuilder sb = new System.Text.StringBuilder ();
redrawAttr = attr;
IEnumerable<int> values = Enum.GetValues (typeof (ConsoleColor))
.OfType<ConsoleColor> ()
.Select (s => (int)s);
if (values.Contains (attr & 0xffff)) {
bg = MapColors ((ConsoleColor)(attr & 0xffff), false);
}
if (values.Contains ((attr >> 16) & 0xffff)) {
fg = MapColors ((ConsoleColor)((attr >> 16) & 0xffff));
}
sb.Append ($"{CSI}{bg};{fg}m");
return sb;
}
int MapColors (ConsoleColor color, bool isForeground = true)
{
switch (color) {
case ConsoleColor.Black:
return isForeground ? COLOR_BLACK : COLOR_BLACK + 10;
case ConsoleColor.DarkBlue:
return isForeground ? COLOR_BLUE : COLOR_BLUE + 10;
case ConsoleColor.DarkGreen:
return isForeground ? COLOR_GREEN : COLOR_GREEN + 10;
case ConsoleColor.DarkCyan:
return isForeground ? COLOR_CYAN : COLOR_CYAN + 10;
case ConsoleColor.DarkRed:
return isForeground ? COLOR_RED : COLOR_RED + 10;
case ConsoleColor.DarkMagenta:
return isForeground ? COLOR_MAGENTA : COLOR_MAGENTA + 10;
case ConsoleColor.DarkYellow:
return isForeground ? COLOR_YELLOW : COLOR_YELLOW + 10;
case ConsoleColor.Gray:
return isForeground ? COLOR_WHITE : COLOR_WHITE + 10;
case ConsoleColor.DarkGray:
return isForeground ? COLOR_BRIGHT_BLACK : COLOR_BRIGHT_BLACK + 10;
case ConsoleColor.Blue:
return isForeground ? COLOR_BRIGHT_BLUE : COLOR_BRIGHT_BLUE + 10;
case ConsoleColor.Green:
return isForeground ? COLOR_BRIGHT_GREEN : COLOR_BRIGHT_GREEN + 10;
case ConsoleColor.Cyan:
return isForeground ? COLOR_BRIGHT_CYAN : COLOR_BRIGHT_CYAN + 10;
case ConsoleColor.Red:
return isForeground ? COLOR_BRIGHT_RED : COLOR_BRIGHT_RED + 10;
case ConsoleColor.Magenta:
return isForeground ? COLOR_BRIGHT_MAGENTA : COLOR_BRIGHT_MAGENTA + 10;
case ConsoleColor.Yellow:
return isForeground ? COLOR_BRIGHT_YELLOW : COLOR_BRIGHT_YELLOW + 10;
case ConsoleColor.White:
return isForeground ? COLOR_BRIGHT_WHITE : COLOR_BRIGHT_WHITE + 10;
}
return 0;
}
bool SetCursorPosition (int col, int row)
{
// 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;
}
}
private CursorVisibility? savedCursorVisibility;
public override void UpdateCursor ()
{
if (!EnsureCursorVisibility ())
return;
// Prevents the exception of size changing during resizing.
try {
if (ccol >= 0 && ccol < Console.BufferWidth && crow >= 0 && crow < Console.BufferHeight) {
Console.SetCursorPosition (ccol, crow);
}
} catch (System.IO.IOException) {
} catch (ArgumentOutOfRangeException) {
}
}
public override void StartReportingMouseMoves ()
{
Console.Out.Write ("\x1b[?1003h\x1b[?1015h\x1b[?1006h");
Console.Out.Flush ();
}
public override void StopReportingMouseMoves ()
{
Console.Out.Write ("\x1b[?1003l\x1b[?1015l\x1b[?1006l");
Console.Out.Flush ();
}
public override void Suspend ()
{
}
public override void SetAttribute (Attribute c)
{
base.SetAttribute (c);
}
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)
{
MapKeyModifiers (keyInfo, (Key)keyInfo.Key);
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.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.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:
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.Alt | ConsoleModifiers.Control)) != 0) {
if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) {
return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta));
}
}
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.Alt | ConsoleModifiers.Control)) != 0) {
if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) {
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;
Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key)
{
if (keyModifiers == null) {
keyModifiers = new KeyModifiers ();
}
Key keyMod = new Key ();
if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) {
keyMod = Key.ShiftMask;
keyModifiers.Shift = true;
}
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) {
keyMod |= Key.CtrlMask;
keyModifiers.Ctrl = true;
}
if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) {
keyMod |= Key.AltMask;
keyModifiers.Alt = true;
}
return keyMod != Key.Null ? keyMod | key : key;
}
Action<KeyEvent> keyHandler;
Action<KeyEvent> keyDownHandler;
Action<KeyEvent> keyUpHandler;
Action<MouseEvent> mouseHandler;
public override void PrepareToRun (MainLoop mainLoop, Action<KeyEvent> keyHandler, Action<KeyEvent> keyDownHandler, Action<KeyEvent> keyUpHandler, Action<MouseEvent> mouseHandler)
{
this.keyHandler = keyHandler;
this.keyDownHandler = keyDownHandler;
this.keyUpHandler = keyUpHandler;
this.mouseHandler = mouseHandler;
var mLoop = mainLoop.Driver as NetMainLoop;
// Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will be simulated to be called.
mLoop.ProcessInput = (e) => ProcessInput (e);
}
void ProcessInput (NetEvents.InputResult inputEvent)
{
switch (inputEvent.EventType) {
case NetEvents.EventType.Key:
ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
if (consoleKeyInfo.Key == ConsoleKey.Packet) {
consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
}
keyModifiers = new KeyModifiers ();
var map = MapKey (consoleKeyInfo);
if (map == (Key)0xffffffff) {
return;
}
if (map == Key.Null) {
keyDownHandler (new KeyEvent (map, keyModifiers));
keyUpHandler (new KeyEvent (map, keyModifiers));
} else {
keyDownHandler (new KeyEvent (map, keyModifiers));
keyHandler (new KeyEvent (map, keyModifiers));
keyUpHandler (new KeyEvent (map, keyModifiers));
}
break;
case NetEvents.EventType.Mouse:
mouseHandler (ToDriverMouse (inputEvent.MouseEvent));
break;
case NetEvents.EventType.WindowSize:
ChangeWin ();
break;
case NetEvents.EventType.WindowPosition:
var newTop = inputEvent.WindowPositionEvent.Top;
var newLeft = inputEvent.WindowPositionEvent.Left;
if (HeightAsBuffer && (top != newTop || left != newLeft)) {
top = newTop;
left = newLeft;
Refresh ();
}
break;
}
}
void ChangeWin ()
{
const int Min_WindowWidth = 14;
Size size = new Size ();
if (!HeightAsBuffer) {
size = new Size (Math.Max (Min_WindowWidth, Console.WindowWidth),
Console.WindowHeight);
top = 0;
left = 0;
} else {
//largestWindowHeight = Math.Max (Console.BufferHeight, largestWindowHeight);
largestWindowHeight = Console.BufferHeight;
size = new Size (Console.BufferWidth, largestWindowHeight);
}
cols = size.Width;
rows = size.Height;
ResizeScreen ();
UpdateOffScreen ();
TerminalResized?.Invoke ();
}
MouseEvent 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 & NetEvents.MouseButtonState.Button1Pressed) != 0) {
mouseFlag |= MouseFlags.Button1Pressed;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button1Released) != 0) {
mouseFlag |= MouseFlags.Button1Released;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button1Clicked) != 0) {
mouseFlag |= MouseFlags.Button1Clicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button1DoubleClicked) != 0) {
mouseFlag |= MouseFlags.Button1DoubleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button1TripleClicked) != 0) {
mouseFlag |= MouseFlags.Button1TripleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button2Pressed) != 0) {
mouseFlag |= MouseFlags.Button2Pressed;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button2Released) != 0) {
mouseFlag |= MouseFlags.Button2Released;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button2Clicked) != 0) {
mouseFlag |= MouseFlags.Button2Clicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button2DoubleClicked) != 0) {
mouseFlag |= MouseFlags.Button2DoubleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button2TrippleClicked) != 0) {
mouseFlag |= MouseFlags.Button2TripleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button3Pressed) != 0) {
mouseFlag |= MouseFlags.Button3Pressed;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button3Released) != 0) {
mouseFlag |= MouseFlags.Button3Released;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button3Clicked) != 0) {
mouseFlag |= MouseFlags.Button3Clicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button3DoubleClicked) != 0) {
mouseFlag |= MouseFlags.Button3DoubleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button3TripleClicked) != 0) {
mouseFlag |= MouseFlags.Button3TripleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonWheeledUp) != 0) {
mouseFlag |= MouseFlags.WheeledUp;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonWheeledDown) != 0) {
mouseFlag |= MouseFlags.WheeledDown;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonWheeledLeft) != 0) {
mouseFlag |= MouseFlags.WheeledLeft;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonWheeledRight) != 0) {
mouseFlag |= MouseFlags.WheeledRight;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button4Pressed) != 0) {
mouseFlag |= MouseFlags.Button4Pressed;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button4Released) != 0) {
mouseFlag |= MouseFlags.Button4Released;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button4Clicked) != 0) {
mouseFlag |= MouseFlags.Button4Clicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button4DoubleClicked) != 0) {
mouseFlag |= MouseFlags.Button4DoubleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.Button4TripleClicked) != 0) {
mouseFlag |= MouseFlags.Button4TripleClicked;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ReportMousePosition) != 0) {
mouseFlag |= MouseFlags.ReportMousePosition;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonShift) != 0) {
mouseFlag |= MouseFlags.ButtonShift;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonCtrl) != 0) {
mouseFlag |= MouseFlags.ButtonCtrl;
}
if ((me.ButtonState & NetEvents.MouseButtonState.ButtonAlt) != 0) {
mouseFlag |= MouseFlags.ButtonAlt;
}
return new MouseEvent () {
X = me.Position.X,
Y = me.Position.Y,
Flags = mouseFlag
};
}
public override Attribute GetAttribute ()
{
return CurrentAttribute;
}
/// <inheritdoc/>
public override bool GetCursorVisibility (out CursorVisibility visibility)
{
visibility = savedCursorVisibility ?? CursorVisibility.Default;
return visibility == CursorVisibility.Default;
}
/// <inheritdoc/>
public override bool SetCursorVisibility (CursorVisibility visibility)
{
savedCursorVisibility = visibility;
return Console.CursorVisible = visibility == CursorVisibility.Default;
}
/// <inheritdoc/>
public override bool EnsureCursorVisibility ()
{
if (!(ccol >= 0 && crow >= 0 && ccol < Cols && crow < Rows)) {
GetCursorVisibility (out CursorVisibility cursorVisibility);
savedCursorVisibility = cursorVisibility;
SetCursorVisibility (CursorVisibility.Invisible);
return false;
}
SetCursorVisibility (savedCursorVisibility ?? CursorVisibility.Default);
return savedCursorVisibility == CursorVisibility.Default;
}
public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
{
NetEvents.InputResult input = new NetEvents.InputResult ();
input.EventType = NetEvents.EventType.Key;
input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control);
try {
ProcessInput (input);
} catch (OverflowException) { }
}
public override bool GetColors (int value, out Color foreground, out Color background)
{
bool hasColor = false;
foreground = default;
background = default;
IEnumerable<int> values = Enum.GetValues (typeof (ConsoleColor))
.OfType<ConsoleColor> ()
.Select (s => (int)s);
if (values.Contains (value & 0xffff)) {
hasColor = true;
background = (Color)(ConsoleColor)(value & 0xffff);
}
if (values.Contains ((value >> 16) & 0xffff)) {
hasColor = true;
foreground = (Color)(ConsoleColor)((value >> 16) & 0xffff);
}
return hasColor;
}
#region Unused
public override void SetColors (short foregroundColorId, short backgroundColorId)
{
}
public override void CookMouse ()
{
}
public override void UncookMouse ()
{
}
#endregion
//
// These are for the .NET driver, but running natively on Windows, wont run
// on the Mono emulation
//
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This implementation is used for NetDriver.
/// </remarks>
internal class NetMainLoop : IMainLoopDriver {
ManualResetEventSlim keyReady = new ManualResetEventSlim (false);
ManualResetEventSlim waitForProbe = new ManualResetEventSlim (false);
Queue<NetEvents.InputResult?> inputResult = new Queue<NetEvents.InputResult?> ();
MainLoop mainLoop;
CancellationTokenSource tokenSource = new CancellationTokenSource ();
NetEvents netEvents;
/// <summary>
/// Invoked when a Key is pressed.
/// </summary>
public Action<NetEvents.InputResult> ProcessInput;
/// <summary>
/// Initializes the class with the console driver.
/// </summary>
/// <remarks>
/// Passing a consoleDriver is provided to capture windows resizing.
/// </remarks>
/// <param name="consoleDriver">The console driver used by this Net main loop.</param>
public NetMainLoop (ConsoleDriver consoleDriver = null)
{
if (consoleDriver == null) {
throw new ArgumentNullException ("Console driver instance must be provided.");
}
netEvents = new NetEvents (consoleDriver);
}
void NetInputHandler ()
{
while (true) {
waitForProbe.Wait ();
waitForProbe.Reset ();
if (inputResult.Count == 0) {
inputResult.Enqueue (netEvents.ReadConsoleInput ());
}
try {
while (inputResult.Peek () == null) {
inputResult.Dequeue ();
}
if (inputResult.Count > 0) {
keyReady.Set ();
}
} catch (InvalidOperationException) { }
}
}
void IMainLoopDriver.Setup (MainLoop mainLoop)
{
this.mainLoop = mainLoop;
Task.Run (NetInputHandler);
}
void IMainLoopDriver.Wakeup ()
{
keyReady.Set ();
}
bool IMainLoopDriver.EventsPending (bool wait)
{
waitForProbe.Set ();
if (CheckTimers (wait, out var waitTimeout)) {
return true;
}
try {
if (!tokenSource.IsCancellationRequested) {
keyReady.Wait (waitTimeout, tokenSource.Token);
}
} catch (OperationCanceledException) {
return true;
} finally {
keyReady.Reset ();
}
if (!tokenSource.IsCancellationRequested) {
return inputResult.Count > 0 || CheckTimers (wait, out _);
}
tokenSource.Dispose ();
tokenSource = new CancellationTokenSource ();
return true;
}
bool CheckTimers (bool wait, out int waitTimeout)
{
long now = DateTime.UtcNow.Ticks;
if (mainLoop.timeouts.Count > 0) {
waitTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
if (waitTimeout < 0)
return true;
} else {
waitTimeout = -1;
}
if (!wait)
waitTimeout = 0;
int ic;
lock (mainLoop.idleHandlers) {
ic = mainLoop.idleHandlers.Count;
}
return ic > 0;
}
void IMainLoopDriver.MainIteration ()
{
while (inputResult.Count > 0) {
ProcessInput?.Invoke (inputResult.Dequeue ().Value);
}
}
}
}