Files
Terminal.Gui/Terminal.Gui/ConsoleDrivers/NetDriver.cs

1833 lines
60 KiB
C#

//
// NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient.
//
using System.Diagnostics;
using System.Runtime.InteropServices;
using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
using static Terminal.Gui.NetEvents;
namespace Terminal.Gui;
internal class NetWinVTConsole
{
private const uint DISABLE_NEWLINE_AUTO_RETURN = 8;
private const uint ENABLE_ECHO_INPUT = 4;
private const uint ENABLE_EXTENDED_FLAGS = 128;
private const uint ENABLE_INSERT_MODE = 32;
private const uint ENABLE_LINE_INPUT = 2;
private const uint ENABLE_LVB_GRID_WORLDWIDE = 10;
private const uint ENABLE_MOUSE_INPUT = 16;
// Input modes.
private const uint ENABLE_PROCESSED_INPUT = 1;
// Output modes.
private const uint ENABLE_PROCESSED_OUTPUT = 1;
private const uint ENABLE_QUICK_EDIT_MODE = 64;
private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
private const uint ENABLE_WINDOW_INPUT = 8;
private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;
private const int STD_ERROR_HANDLE = -12;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
private readonly nint _errorHandle;
private readonly nint _inputHandle;
private readonly uint _originalErrorConsoleMode;
private readonly uint _originalInputConsoleMode;
private readonly uint _originalOutputConsoleMode;
private readonly nint _outputHandle;
public NetWinVTConsole ()
{
_inputHandle = GetStdHandle (STD_INPUT_HANDLE);
if (!GetConsoleMode (_inputHandle, out uint mode))
{
throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
}
_originalInputConsoleMode = mode;
if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
{
mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
if (!SetConsoleMode (_inputHandle, mode))
{
throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
}
}
_outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
if (!GetConsoleMode (_outputHandle, out mode))
{
throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
}
_originalOutputConsoleMode = mode;
if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
{
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode (_outputHandle, mode))
{
throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
}
}
_errorHandle = GetStdHandle (STD_ERROR_HANDLE);
if (!GetConsoleMode (_errorHandle, out mode))
{
throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
}
_originalErrorConsoleMode = mode;
if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
{
mode |= DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode (_errorHandle, mode))
{
throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
}
}
}
public void Cleanup ()
{
if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode))
{
throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}.");
}
if (!SetConsoleMode (_outputHandle, _originalOutputConsoleMode))
{
throw new ApplicationException ($"Failed to restore output console mode, error code: {GetLastError ()}.");
}
if (!SetConsoleMode (_errorHandle, _originalErrorConsoleMode))
{
throw new ApplicationException ($"Failed to restore error console mode, error code: {GetLastError ()}.");
}
}
[DllImport ("kernel32.dll")]
private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);
[DllImport ("kernel32.dll")]
private static extern uint GetLastError ();
[DllImport ("kernel32.dll", SetLastError = true)]
private static extern nint GetStdHandle (int nStdHandle);
[DllImport ("kernel32.dll")]
private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
}
internal class NetEvents : IDisposable
{
private readonly ManualResetEventSlim _inputReady = new (false);
private CancellationTokenSource _inputReadyCancellationTokenSource;
private readonly ManualResetEventSlim _waitForStart = new (false);
//CancellationTokenSource _waitForStartCancellationTokenSource;
private readonly ManualResetEventSlim _winChange = new (false);
private readonly Queue<InputResult?> _inputQueue = new ();
private readonly ConsoleDriver _consoleDriver;
private ConsoleKeyInfo [] _cki;
private bool _isEscSeq;
#if PROCESS_REQUEST
bool _neededProcessRequest;
#endif
public EscSeqRequests EscSeqRequests { get; } = new ();
public NetEvents (ConsoleDriver consoleDriver)
{
_consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
_inputReadyCancellationTokenSource = new CancellationTokenSource ();
Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
}
public InputResult? DequeueInput ()
{
while (_inputReadyCancellationTokenSource != null
&& !_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
{
_waitForStart.Set ();
_winChange.Set ();
try
{
if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
{
if (_inputQueue.Count == 0)
{
_inputReady.Wait (_inputReadyCancellationTokenSource.Token);
}
}
}
catch (OperationCanceledException)
{
return null;
}
finally
{
_inputReady.Reset ();
}
#if PROCESS_REQUEST
_neededProcessRequest = false;
#endif
if (_inputQueue.Count > 0)
{
return _inputQueue.Dequeue ();
}
}
return null;
}
private static ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
{
// if there is a key available, return it without waiting
// (or dispatching work to the thread queue)
if (Console.KeyAvailable)
{
return Console.ReadKey (intercept);
}
while (!cancellationToken.IsCancellationRequested)
{
Task.Delay (100, cancellationToken).Wait (cancellationToken);
if (Console.KeyAvailable)
{
return Console.ReadKey (intercept);
}
}
cancellationToken.ThrowIfCancellationRequested ();
return default (ConsoleKeyInfo);
}
private void ProcessInputQueue ()
{
while (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
{
try
{
_waitForStart.Wait (_inputReadyCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
_waitForStart.Reset ();
if (_inputQueue.Count == 0)
{
ConsoleKey key = 0;
ConsoleModifiers mod = 0;
ConsoleKeyInfo newConsoleKeyInfo = default;
while (true)
{
if (_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
{
return;
}
ConsoleKeyInfo consoleKeyInfo;
try
{
consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
|| (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
{
if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
{
_cki = EscSeqUtils.ResizeArray (
new ConsoleKeyInfo (
(char)KeyCode.Esc,
0,
false,
false,
false
),
_cki
);
}
_isEscSeq = true;
newConsoleKeyInfo = consoleKeyInfo;
_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
if (Console.KeyAvailable)
{
continue;
}
ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
_cki = null;
_isEscSeq = false;
break;
}
if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
{
ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
_cki = null;
if (Console.KeyAvailable)
{
_cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
}
else
{
ProcessMapConsoleKeyInfo (consoleKeyInfo);
}
break;
}
ProcessMapConsoleKeyInfo (consoleKeyInfo);
break;
}
}
_inputReady.Set ();
}
void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
{
_inputQueue.Enqueue (
new InputResult
{
EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
}
);
_isEscSeq = false;
}
}
private void CheckWindowSizeChange ()
{
void RequestWindowSize (CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// Wait for a while then check if screen has changed sizes
Task.Delay (500, cancellationToken).Wait (cancellationToken);
int buffHeight, buffWidth;
if (((NetDriver)_consoleDriver).IsWinPlatform)
{
buffHeight = Math.Max (Console.BufferHeight, 0);
buffWidth = Math.Max (Console.BufferWidth, 0);
}
else
{
buffHeight = _consoleDriver.Rows;
buffWidth = _consoleDriver.Cols;
}
if (EnqueueWindowSizeEvent (
Math.Max (Console.WindowHeight, 0),
Math.Max (Console.WindowWidth, 0),
buffHeight,
buffWidth
))
{
return;
}
}
cancellationToken.ThrowIfCancellationRequested ();
}
while (true)
{
if (_inputReadyCancellationTokenSource.IsCancellationRequested)
{
return;
}
try
{
_winChange.Wait (_inputReadyCancellationTokenSource.Token);
_winChange.Reset ();
RequestWindowSize (_inputReadyCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
_inputReady.Set ();
}
}
/// <summary>Enqueue a window size event if the window size has changed.</summary>
/// <param name="winHeight"></param>
/// <param name="winWidth"></param>
/// <param name="buffHeight"></param>
/// <param name="buffWidth"></param>
/// <returns></returns>
private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
{
if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows)
{
return false;
}
int w = Math.Max (winWidth, 0);
int h = Math.Max (winHeight, 0);
_inputQueue.Enqueue (
new InputResult
{
EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) }
}
);
return true;
}
// Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
private void ProcessRequestResponse (
ref ConsoleKeyInfo newConsoleKeyInfo,
ref ConsoleKey key,
ConsoleKeyInfo [] cki,
ref ConsoleModifiers mod
)
{
// isMouse is true if it's CSI<, false otherwise
EscSeqUtils.DecodeEscSeq (
EscSeqRequests,
ref newConsoleKeyInfo,
ref key,
cki,
ref mod,
out string c1Control,
out string code,
out string [] values,
out string terminating,
out bool isMouse,
out List<MouseFlags> mouseFlags,
out Point pos,
out bool isReq,
(f, p) => HandleMouseEvent (MapMouseFlags (f), p)
);
if (isMouse)
{
foreach (MouseFlags mf in mouseFlags)
{
HandleMouseEvent (MapMouseFlags (mf), pos);
}
return;
}
if (isReq)
{
HandleRequestResponseEvent (c1Control, code, values, terminating);
return;
}
HandleKeyboardEvent (newConsoleKeyInfo);
}
private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
{
MouseButtonState mbs = default;
foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
{
if (mouseFlags.HasFlag ((MouseFlags)flag))
{
switch (flag)
{
case MouseFlags.Button1Pressed:
mbs |= MouseButtonState.Button1Pressed;
break;
case MouseFlags.Button1Released:
mbs |= MouseButtonState.Button1Released;
break;
case MouseFlags.Button1Clicked:
mbs |= MouseButtonState.Button1Clicked;
break;
case MouseFlags.Button1DoubleClicked:
mbs |= MouseButtonState.Button1DoubleClicked;
break;
case MouseFlags.Button1TripleClicked:
mbs |= MouseButtonState.Button1TripleClicked;
break;
case MouseFlags.Button2Pressed:
mbs |= MouseButtonState.Button2Pressed;
break;
case MouseFlags.Button2Released:
mbs |= MouseButtonState.Button2Released;
break;
case MouseFlags.Button2Clicked:
mbs |= MouseButtonState.Button2Clicked;
break;
case MouseFlags.Button2DoubleClicked:
mbs |= MouseButtonState.Button2DoubleClicked;
break;
case MouseFlags.Button2TripleClicked:
mbs |= MouseButtonState.Button2TripleClicked;
break;
case MouseFlags.Button3Pressed:
mbs |= MouseButtonState.Button3Pressed;
break;
case MouseFlags.Button3Released:
mbs |= MouseButtonState.Button3Released;
break;
case MouseFlags.Button3Clicked:
mbs |= MouseButtonState.Button3Clicked;
break;
case MouseFlags.Button3DoubleClicked:
mbs |= MouseButtonState.Button3DoubleClicked;
break;
case MouseFlags.Button3TripleClicked:
mbs |= MouseButtonState.Button3TripleClicked;
break;
case MouseFlags.WheeledUp:
mbs |= MouseButtonState.ButtonWheeledUp;
break;
case MouseFlags.WheeledDown:
mbs |= MouseButtonState.ButtonWheeledDown;
break;
case MouseFlags.WheeledLeft:
mbs |= MouseButtonState.ButtonWheeledLeft;
break;
case MouseFlags.WheeledRight:
mbs |= MouseButtonState.ButtonWheeledRight;
break;
case MouseFlags.Button4Pressed:
mbs |= MouseButtonState.Button4Pressed;
break;
case MouseFlags.Button4Released:
mbs |= MouseButtonState.Button4Released;
break;
case MouseFlags.Button4Clicked:
mbs |= MouseButtonState.Button4Clicked;
break;
case MouseFlags.Button4DoubleClicked:
mbs |= MouseButtonState.Button4DoubleClicked;
break;
case MouseFlags.Button4TripleClicked:
mbs |= MouseButtonState.Button4TripleClicked;
break;
case MouseFlags.ButtonShift:
mbs |= MouseButtonState.ButtonShift;
break;
case MouseFlags.ButtonCtrl:
mbs |= MouseButtonState.ButtonCtrl;
break;
case MouseFlags.ButtonAlt:
mbs |= MouseButtonState.ButtonAlt;
break;
case MouseFlags.ReportMousePosition:
mbs |= MouseButtonState.ReportMousePosition;
break;
case MouseFlags.AllEvents:
mbs |= MouseButtonState.AllEvents;
break;
}
}
}
return mbs;
}
private Point _lastCursorPosition;
private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
{
switch (terminating)
{
// BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
case EscSeqUtils.CSI_RequestCursorPositionReport_Terminator:
var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
if (_lastCursorPosition.Y != point.Y)
{
_lastCursorPosition = point;
var eventType = EventType.WindowPosition;
var winPositionEv = new WindowPositionEvent { CursorPosition = point };
_inputQueue.Enqueue (
new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
);
}
else
{
return;
}
break;
case EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator:
switch (values [0])
{
case EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue:
EnqueueWindowSizeEvent (
Math.Max (int.Parse (values [1]), 0),
Math.Max (int.Parse (values [2]), 0),
Math.Max (int.Parse (values [1]), 0),
Math.Max (int.Parse (values [2]), 0)
);
break;
default:
EnqueueRequestResponseEvent (c1Control, code, values, terminating);
break;
}
break;
default:
EnqueueRequestResponseEvent (c1Control, code, values, terminating);
break;
}
_inputReady.Set ();
}
private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
{
var eventType = EventType.RequestResponse;
var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
_inputQueue.Enqueue (
new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
);
}
private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
{
var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
_inputQueue.Enqueue (
new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent }
);
_inputReady.Set ();
}
public enum EventType
{
Key = 1,
Mouse = 2,
WindowSize = 3,
WindowPosition = 4,
RequestResponse = 5
}
[Flags]
public enum MouseButtonState
{
Button1Pressed = 0x1,
Button1Released = 0x2,
Button1Clicked = 0x4,
Button1DoubleClicked = 0x8,
Button1TripleClicked = 0x10,
Button2Pressed = 0x20,
Button2Released = 0x40,
Button2Clicked = 0x80,
Button2DoubleClicked = 0x100,
Button2TripleClicked = 0x200,
Button3Pressed = 0x400,
Button3Released = 0x800,
Button3Clicked = 0x1000,
Button3DoubleClicked = 0x2000,
Button3TripleClicked = 0x4000,
ButtonWheeledUp = 0x8000,
ButtonWheeledDown = 0x10000,
ButtonWheeledLeft = 0x20000,
ButtonWheeledRight = 0x40000,
Button4Pressed = 0x80000,
Button4Released = 0x100000,
Button4Clicked = 0x200000,
Button4DoubleClicked = 0x400000,
Button4TripleClicked = 0x800000,
ButtonShift = 0x1000000,
ButtonCtrl = 0x2000000,
ButtonAlt = 0x4000000,
ReportMousePosition = 0x8000000,
AllEvents = -1
}
public struct MouseEvent
{
public Point Position;
public MouseButtonState ButtonState;
}
public struct WindowSizeEvent
{
public Size Size;
}
public struct WindowPositionEvent
{
public int Top;
public int Left;
public Point CursorPosition;
}
public struct RequestResponseEvent
{
public (string c1Control, string code, string [] values, string terminating) ResultTuple;
}
public struct InputResult
{
public EventType EventType;
public ConsoleKeyInfo ConsoleKeyInfo;
public MouseEvent MouseEvent;
public WindowSizeEvent WindowSizeEvent;
public WindowPositionEvent WindowPositionEvent;
public RequestResponseEvent RequestResponseEvent;
public readonly override string ToString ()
{
return EventType switch
{
EventType.Key => ToString (ConsoleKeyInfo),
EventType.Mouse => MouseEvent.ToString (),
//EventType.WindowSize => WindowSize.ToString (),
//EventType.RequestResponse => RequestResponse.ToString (),
_ => "Unknown event type: " + EventType
};
}
/// <summary>Prints a ConsoleKeyInfoEx structure</summary>
/// <param name="cki"></param>
/// <returns></returns>
public readonly string ToString (ConsoleKeyInfo cki)
{
var ke = new Key ((KeyCode)cki.KeyChar);
var sb = new StringBuilder ();
sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
return $"[ConsoleKeyInfo({s})]";
}
}
private void HandleKeyboardEvent (ConsoleKeyInfo cki)
{
var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
_inputQueue.Enqueue (inputResult);
}
public void Dispose ()
{
_inputReadyCancellationTokenSource?.Cancel ();
_inputReadyCancellationTokenSource?.Dispose ();
_inputReadyCancellationTokenSource = null;
try
{
// throws away any typeahead that has been typed by
// the user and has not yet been read by the program.
while (Console.KeyAvailable)
{
Console.ReadKey (true);
}
}
catch (InvalidOperationException)
{
// Ignore - Console input has already been closed
}
}
}
internal class NetDriver : ConsoleDriver
{
private const int COLOR_BLACK = 30;
private const int COLOR_BLUE = 34;
private const int COLOR_BRIGHT_BLACK = 90;
private const int COLOR_BRIGHT_BLUE = 94;
private const int COLOR_BRIGHT_CYAN = 96;
private const int COLOR_BRIGHT_GREEN = 92;
private const int COLOR_BRIGHT_MAGENTA = 95;
private const int COLOR_BRIGHT_RED = 91;
private const int COLOR_BRIGHT_WHITE = 97;
private const int COLOR_BRIGHT_YELLOW = 93;
private const int COLOR_CYAN = 36;
private const int COLOR_GREEN = 32;
private const int COLOR_MAGENTA = 35;
private const int COLOR_RED = 31;
private const int COLOR_WHITE = 37;
private const int COLOR_YELLOW = 33;
private NetMainLoop _mainLoopDriver;
public bool IsWinPlatform { get; private set; }
public NetWinVTConsole NetWinConsole { get; private set; }
public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix
|| (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931);
public override void Refresh ()
{
UpdateScreen ();
UpdateCursor ();
}
public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
{
var input = new InputResult
{
EventType = EventType.Key, ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, shift, alt, control)
};
try
{
ProcessInput (input);
}
catch (OverflowException)
{ }
}
public override void Suspend ()
{
if (Environment.OSVersion.Platform != PlatformID.Unix)
{
return;
}
StopReportingMouseMoves ();
if (!RunningUnitTests)
{
Console.ResetColor ();
Console.Clear ();
//Disable alternative screen buffer.
Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
//Set cursor key to cursor.
Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
Platform.Suspend ();
//Enable alternative screen buffer.
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
SetContentsAsDirty ();
Refresh ();
}
StartReportingMouseMoves ();
}
public override void UpdateScreen ()
{
if (RunningUnitTests
|| _winSizeChanging
|| Console.WindowHeight < 1
|| Contents.Length != Rows * Cols
|| Rows != Console.WindowHeight)
{
return;
}
var top = 0;
var left = 0;
int rows = Rows;
int cols = Cols;
var output = new StringBuilder ();
Attribute? redrawAttr = null;
int lastCol = -1;
CursorVisibility? savedVisibility = _cachedCursorVisibility;
SetCursorVisibility (CursorVisibility.Invisible);
for (int row = top; row < rows; row++)
{
if (Console.WindowHeight < 1)
{
return;
}
if (!_dirtyLines [row])
{
continue;
}
if (!SetCursorPosition (0, row))
{
return;
}
_dirtyLines [row] = false;
output.Clear ();
for (int col = left; col < cols; col++)
{
lastCol = -1;
var outputWidth = 0;
for (; col < cols; col++)
{
if (!Contents [row, col].IsDirty)
{
if (output.Length > 0)
{
WriteToConsole (output, ref lastCol, row, ref outputWidth);
}
else if (lastCol == -1)
{
lastCol = col;
}
if (lastCol + 1 < cols)
{
lastCol++;
}
continue;
}
if (lastCol == -1)
{
lastCol = col;
}
Attribute attr = Contents [row, col].Attribute.Value;
// Performance: Only send the escape sequence if the attribute has changed.
if (attr != redrawAttr)
{
redrawAttr = attr;
if (Force16Colors)
{
output.Append (
EscSeqUtils.CSI_SetGraphicsRendition (
MapColors (
(ConsoleColor)attr.Background.GetClosestNamedColor (),
false
),
MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor ())
)
);
}
else
{
output.Append (
EscSeqUtils.CSI_SetForegroundColorRGB (
attr.Foreground.R,
attr.Foreground.G,
attr.Foreground.B
)
);
output.Append (
EscSeqUtils.CSI_SetBackgroundColorRGB (
attr.Background.R,
attr.Background.G,
attr.Background.B
)
);
}
}
outputWidth++;
Rune rune = Contents [row, col].Rune;
output.Append (rune);
if (Contents [row, col].CombiningMarks.Count > 0)
{
// AtlasEngine does not support NON-NORMALIZED combining marks in a way
// compatible with the driver architecture. Any CMs (except in the first col)
// are correctly combined with the base char, but are ALSO treated as 1 column
// width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`.
//
// For now, we just ignore the list of CMs.
//foreach (var combMark in Contents [row, col].CombiningMarks) {
// output.Append (combMark);
//}
// WriteToConsole (output, ref lastCol, row, ref outputWidth);
}
else if (rune.IsSurrogatePair () && rune.GetColumns () < 2)
{
WriteToConsole (output, ref lastCol, row, ref outputWidth);
SetCursorPosition (col - 1, row);
}
Contents [row, col].IsDirty = false;
}
}
if (output.Length > 0)
{
SetCursorPosition (lastCol, row);
Console.Write (output);
}
}
SetCursorPosition (0, 0);
_cachedCursorVisibility = savedVisibility;
void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
{
SetCursorPosition (lastCol, row);
Console.Write (output);
output.Clear ();
lastCol += outputWidth;
outputWidth = 0;
}
}
internal override void End ()
{
if (IsWinPlatform)
{
NetWinConsole?.Cleanup ();
}
StopReportingMouseMoves ();
if (!RunningUnitTests)
{
Console.ResetColor ();
//Disable alternative screen buffer.
Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
//Set cursor key to cursor.
Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
Console.Out.Close ();
}
}
internal override MainLoop Init ()
{
PlatformID p = Environment.OSVersion.Platform;
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
{
IsWinPlatform = true;
try
{
NetWinConsole = new NetWinVTConsole ();
}
catch (ApplicationException)
{
// Likely running as a unit test, or in a non-interactive session.
}
}
if (IsWinPlatform)
{
Clipboard = new WindowsClipboard ();
}
else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
{
Clipboard = new MacOSXClipboard ();
}
else
{
if (CursesDriver.Is_WSL_Platform ())
{
Clipboard = new WSLClipboard ();
}
else
{
Clipboard = new CursesClipboard ();
}
}
if (!RunningUnitTests)
{
Console.TreatControlCAsInput = true;
Cols = Console.WindowWidth;
Rows = Console.WindowHeight;
//Enable alternative screen buffer.
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
//Set cursor key to application.
Console.Out.Write (EscSeqUtils.CSI_HideCursor);
}
else
{
// We are being run in an environment that does not support a console
// such as a unit test, or a pipe.
Cols = 80;
Rows = 24;
}
ResizeScreen ();
ClearContents ();
CurrentAttribute = new Attribute (Color.White, Color.Black);
StartReportingMouseMoves ();
_mainLoopDriver = new NetMainLoop (this);
_mainLoopDriver.ProcessInput = ProcessInput;
return new MainLoop (_mainLoopDriver);
}
private void ProcessInput (InputResult inputEvent)
{
switch (inputEvent.EventType)
{
case EventType.Key:
ConsoleKeyInfo consoleKeyInfo = inputEvent.ConsoleKeyInfo;
//if (consoleKeyInfo.Key == ConsoleKey.Packet) {
// consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo);
//}
//Debug.WriteLine ($"event: {inputEvent}");
KeyCode map = MapKey (consoleKeyInfo);
if (map == KeyCode.Null)
{
break;
}
OnKeyDown (new Key (map));
OnKeyUp (new Key (map));
break;
case EventType.Mouse:
MouseEvent me = ToDriverMouse (inputEvent.MouseEvent);
//Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}");
OnMouseEvent (me);
break;
case EventType.WindowSize:
_winSizeChanging = true;
Top = 0;
Left = 0;
Cols = inputEvent.WindowSizeEvent.Size.Width;
Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0);
;
ResizeScreen ();
ClearContents ();
_winSizeChanging = false;
OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
break;
case EventType.RequestResponse:
break;
case EventType.WindowPosition:
break;
default:
throw new ArgumentOutOfRangeException ();
}
}
#region Size and Position Handling
private volatile bool _winSizeChanging;
private void SetWindowPosition (int col, int row)
{
if (!RunningUnitTests)
{
Top = Console.WindowTop;
Left = Console.WindowLeft;
}
else
{
Top = row;
Left = col;
}
}
public virtual void ResizeScreen ()
{
// Not supported on Unix.
if (IsWinPlatform)
{
// Can raise an exception while is still resizing.
try
{
#pragma warning disable CA1416
if (Console.WindowHeight > 0)
{
Console.CursorTop = 0;
Console.CursorLeft = 0;
Console.WindowTop = 0;
Console.WindowLeft = 0;
if (Console.WindowHeight > Rows)
{
Console.SetWindowSize (Cols, Rows);
}
Console.SetBufferSize (Cols, Rows);
}
#pragma warning restore CA1416
}
// INTENT: Why are these eating the exceptions?
// Comments would be good here.
catch (IOException)
{
// CONCURRENCY: Unsynchronized access to Clip is not safe.
Clip = new (0, 0, Cols, Rows);
}
catch (ArgumentOutOfRangeException)
{
// CONCURRENCY: Unsynchronized access to Clip is not safe.
Clip = new (0, 0, Cols, Rows);
}
}
else
{
Console.Out.Write (EscSeqUtils.CSI_SetTerminalWindowSize (Rows, Cols));
}
// CONCURRENCY: Unsynchronized access to Clip is not safe.
Clip = new (0, 0, Cols, Rows);
}
#endregion
#region Color Handling
// Cache the list of ConsoleColor values.
private static readonly HashSet<int> ConsoleColorValues = new (
Enum.GetValues (typeof (ConsoleColor))
.OfType<ConsoleColor> ()
.Select (c => (int)c)
);
// Dictionary for mapping ConsoleColor values to the values used by System.Net.Console.
private static readonly Dictionary<ConsoleColor, int> colorMap = new ()
{
{ ConsoleColor.Black, COLOR_BLACK },
{ ConsoleColor.DarkBlue, COLOR_BLUE },
{ ConsoleColor.DarkGreen, COLOR_GREEN },
{ ConsoleColor.DarkCyan, COLOR_CYAN },
{ ConsoleColor.DarkRed, COLOR_RED },
{ ConsoleColor.DarkMagenta, COLOR_MAGENTA },
{ ConsoleColor.DarkYellow, COLOR_YELLOW },
{ ConsoleColor.Gray, COLOR_WHITE },
{ ConsoleColor.DarkGray, COLOR_BRIGHT_BLACK },
{ ConsoleColor.Blue, COLOR_BRIGHT_BLUE },
{ ConsoleColor.Green, COLOR_BRIGHT_GREEN },
{ ConsoleColor.Cyan, COLOR_BRIGHT_CYAN },
{ ConsoleColor.Red, COLOR_BRIGHT_RED },
{ ConsoleColor.Magenta, COLOR_BRIGHT_MAGENTA },
{ ConsoleColor.Yellow, COLOR_BRIGHT_YELLOW },
{ ConsoleColor.White, COLOR_BRIGHT_WHITE }
};
// Map a ConsoleColor to a platform dependent value.
private int MapColors (ConsoleColor color, bool isForeground = true)
{
return colorMap.TryGetValue (color, out int colorValue) ? colorValue + (isForeground ? 0 : 10) : 0;
}
///// <remarks>
///// In the NetDriver, colors are encoded as an int.
///// However, the foreground color is stored in the most significant 16 bits,
///// and the background color is stored in the least significant 16 bits.
///// </remarks>
//public override Attribute MakeColor (Color foreground, Color background)
//{
// // Encode the colors into the int value.
// return new Attribute (
// platformColor: ((((int)foreground.ColorName) & 0xffff) << 16) | (((int)background.ColorName) & 0xffff),
// foreground: foreground,
// background: background
// );
//}
#endregion
#region Cursor Handling
private bool SetCursorPosition (int col, int row)
{
if (IsWinPlatform)
{
// Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth.
try
{
Console.SetCursorPosition (col, row);
return true;
}
catch (Exception)
{
return false;
}
}
// + 1 is needed because non-Windows is based on 1 instead of 0 and
// Console.CursorTop/CursorLeft isn't reliable.
Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
return true;
}
private CursorVisibility? _cachedCursorVisibility;
public override void UpdateCursor ()
{
EnsureCursorVisibility ();
if (Col >= 0 && Col < Cols && Row >= 0 && Row <= Rows)
{
SetCursorPosition (Col, Row);
SetWindowPosition (0, Row);
}
}
public override bool GetCursorVisibility (out CursorVisibility visibility)
{
visibility = _cachedCursorVisibility ?? CursorVisibility.Default;
return visibility == CursorVisibility.Default;
}
public override bool SetCursorVisibility (CursorVisibility visibility)
{
_cachedCursorVisibility = visibility;
Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
return visibility == CursorVisibility.Default;
}
public override bool EnsureCursorVisibility ()
{
if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
{
GetCursorVisibility (out CursorVisibility cursorVisibility);
_cachedCursorVisibility = cursorVisibility;
SetCursorVisibility (CursorVisibility.Invisible);
return false;
}
SetCursorVisibility (_cachedCursorVisibility ?? CursorVisibility.Default);
return _cachedCursorVisibility == CursorVisibility.Default;
}
#endregion
#region Mouse Handling
public void StartReportingMouseMoves ()
{
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
}
}
public void StopReportingMouseMoves ()
{
if (!RunningUnitTests)
{
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
}
}
private 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 & MouseButtonState.Button1Pressed) != 0)
{
mouseFlag |= MouseFlags.Button1Pressed;
}
if ((me.ButtonState & MouseButtonState.Button1Released) != 0)
{
mouseFlag |= MouseFlags.Button1Released;
}
if ((me.ButtonState & MouseButtonState.Button1Clicked) != 0)
{
mouseFlag |= MouseFlags.Button1Clicked;
}
if ((me.ButtonState & MouseButtonState.Button1DoubleClicked) != 0)
{
mouseFlag |= MouseFlags.Button1DoubleClicked;
}
if ((me.ButtonState & MouseButtonState.Button1TripleClicked) != 0)
{
mouseFlag |= MouseFlags.Button1TripleClicked;
}
if ((me.ButtonState & MouseButtonState.Button2Pressed) != 0)
{
mouseFlag |= MouseFlags.Button2Pressed;
}
if ((me.ButtonState & MouseButtonState.Button2Released) != 0)
{
mouseFlag |= MouseFlags.Button2Released;
}
if ((me.ButtonState & MouseButtonState.Button2Clicked) != 0)
{
mouseFlag |= MouseFlags.Button2Clicked;
}
if ((me.ButtonState & MouseButtonState.Button2DoubleClicked) != 0)
{
mouseFlag |= MouseFlags.Button2DoubleClicked;
}
if ((me.ButtonState & MouseButtonState.Button2TripleClicked) != 0)
{
mouseFlag |= MouseFlags.Button2TripleClicked;
}
if ((me.ButtonState & MouseButtonState.Button3Pressed) != 0)
{
mouseFlag |= MouseFlags.Button3Pressed;
}
if ((me.ButtonState & MouseButtonState.Button3Released) != 0)
{
mouseFlag |= MouseFlags.Button3Released;
}
if ((me.ButtonState & MouseButtonState.Button3Clicked) != 0)
{
mouseFlag |= MouseFlags.Button3Clicked;
}
if ((me.ButtonState & MouseButtonState.Button3DoubleClicked) != 0)
{
mouseFlag |= MouseFlags.Button3DoubleClicked;
}
if ((me.ButtonState & MouseButtonState.Button3TripleClicked) != 0)
{
mouseFlag |= MouseFlags.Button3TripleClicked;
}
if ((me.ButtonState & MouseButtonState.ButtonWheeledUp) != 0)
{
mouseFlag |= MouseFlags.WheeledUp;
}
if ((me.ButtonState & MouseButtonState.ButtonWheeledDown) != 0)
{
mouseFlag |= MouseFlags.WheeledDown;
}
if ((me.ButtonState & MouseButtonState.ButtonWheeledLeft) != 0)
{
mouseFlag |= MouseFlags.WheeledLeft;
}
if ((me.ButtonState & MouseButtonState.ButtonWheeledRight) != 0)
{
mouseFlag |= MouseFlags.WheeledRight;
}
if ((me.ButtonState & MouseButtonState.Button4Pressed) != 0)
{
mouseFlag |= MouseFlags.Button4Pressed;
}
if ((me.ButtonState & MouseButtonState.Button4Released) != 0)
{
mouseFlag |= MouseFlags.Button4Released;
}
if ((me.ButtonState & MouseButtonState.Button4Clicked) != 0)
{
mouseFlag |= MouseFlags.Button4Clicked;
}
if ((me.ButtonState & MouseButtonState.Button4DoubleClicked) != 0)
{
mouseFlag |= MouseFlags.Button4DoubleClicked;
}
if ((me.ButtonState & MouseButtonState.Button4TripleClicked) != 0)
{
mouseFlag |= MouseFlags.Button4TripleClicked;
}
if ((me.ButtonState & MouseButtonState.ReportMousePosition) != 0)
{
mouseFlag |= MouseFlags.ReportMousePosition;
}
if ((me.ButtonState & MouseButtonState.ButtonShift) != 0)
{
mouseFlag |= MouseFlags.ButtonShift;
}
if ((me.ButtonState & MouseButtonState.ButtonCtrl) != 0)
{
mouseFlag |= MouseFlags.ButtonCtrl;
}
if ((me.ButtonState & MouseButtonState.ButtonAlt) != 0)
{
mouseFlag |= MouseFlags.ButtonAlt;
}
return new MouseEvent { Position = me.Position, Flags = mouseFlag };
}
#endregion Mouse Handling
#region Keyboard Handling
private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
{
if (consoleKeyInfo.Key != ConsoleKey.Packet)
{
return consoleKeyInfo;
}
ConsoleModifiers mod = consoleKeyInfo.Modifiers;
bool shift = (mod & ConsoleModifiers.Shift) != 0;
bool alt = (mod & ConsoleModifiers.Alt) != 0;
bool control = (mod & ConsoleModifiers.Control) != 0;
ConsoleKeyInfo cKeyInfo = DecodeVKPacketToKConsoleKeyInfo (consoleKeyInfo);
return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control);
}
private KeyCode MapKey (ConsoleKeyInfo keyInfo)
{
switch (keyInfo.Key)
{
case ConsoleKey.OemPeriod:
case ConsoleKey.OemComma:
case ConsoleKey.OemPlus:
case ConsoleKey.OemMinus:
case ConsoleKey.Packet:
case ConsoleKey.Oem1:
case ConsoleKey.Oem2:
case ConsoleKey.Oem3:
case ConsoleKey.Oem4:
case ConsoleKey.Oem5:
case ConsoleKey.Oem6:
case ConsoleKey.Oem7:
case ConsoleKey.Oem8:
case ConsoleKey.Oem102:
if (keyInfo.KeyChar == 0)
{
// If the keyChar is 0, keyInfo.Key value is not a printable character.
return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
}
if (keyInfo.Modifiers != ConsoleModifiers.Shift)
{
// If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar);
}
// Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç")
// and passing on Shift would be redundant.
return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar);
}
// Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
{
if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
{
return KeyCode.Tab;
}
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key));
}
// Handle control keys (e.g. CursorUp)
if (keyInfo.Key != ConsoleKey.None
&& Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint))
{
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint));
}
if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z)
{
// Shifted
keyInfo = new ConsoleKeyInfo (
keyInfo.KeyChar,
(ConsoleKey)keyInfo.KeyChar,
true,
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
}
if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z)
{
// Unshifted
keyInfo = new ConsoleKeyInfo (
keyInfo.KeyChar,
(ConsoleKey)(keyInfo.KeyChar - 32),
false,
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt),
keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control));
}
if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z )
{
if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)
|| keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control))
{
// NetDriver doesn't support Shift-Ctrl/Shift-Alt combos
return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key);
}
if (keyInfo.Modifiers == ConsoleModifiers.Shift)
{
// If ShiftMask is on add the ShiftMask
if (char.IsUpper (keyInfo.KeyChar))
{
return (KeyCode)keyInfo.Key | KeyCode.ShiftMask;
}
}
return (KeyCode)keyInfo.Key;
}
return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar));
}
#endregion Keyboard Handling
}
/// <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
{
internal NetEvents _netEvents;
/// <summary>Invoked when a Key is pressed.</summary>
internal Action<InputResult> ProcessInput;
private readonly ManualResetEventSlim _eventReady = new (false);
private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
private readonly Queue<InputResult?> _resultQueue = new ();
private readonly ManualResetEventSlim _waitForProbe = new (false);
private CancellationTokenSource _eventReadyTokenSource = new ();
private MainLoop _mainLoop;
/// <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>
/// <exception cref="ArgumentNullException"></exception>
public NetMainLoop (ConsoleDriver consoleDriver = null)
{
if (consoleDriver is null)
{
throw new ArgumentNullException (nameof (consoleDriver));
}
_netEvents = new NetEvents (consoleDriver);
}
void IMainLoopDriver.Setup (MainLoop mainLoop)
{
_mainLoop = mainLoop;
Task.Run (NetInputHandler, _inputHandlerTokenSource.Token);
}
void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
bool IMainLoopDriver.EventsPending ()
{
_waitForProbe.Set ();
if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
{
return true;
}
try
{
if (!_eventReadyTokenSource.IsCancellationRequested)
{
// Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there
// are no timers, but there IS an idle handler waiting.
_eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return true;
}
finally
{
_eventReady.Reset ();
}
if (!_eventReadyTokenSource.IsCancellationRequested)
{
return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
}
_eventReadyTokenSource.Dispose ();
_eventReadyTokenSource = new CancellationTokenSource ();
return true;
}
void IMainLoopDriver.Iteration ()
{
while (_resultQueue.Count > 0)
{
ProcessInput?.Invoke (_resultQueue.Dequeue ().Value);
}
}
void IMainLoopDriver.TearDown ()
{
_inputHandlerTokenSource?.Cancel ();
_inputHandlerTokenSource?.Dispose ();
_eventReadyTokenSource?.Cancel ();
_eventReadyTokenSource?.Dispose ();
_eventReady?.Dispose ();
_resultQueue?.Clear ();
_waitForProbe?.Dispose ();
_netEvents?.Dispose ();
_netEvents = null;
_mainLoop = null;
}
private void NetInputHandler ()
{
while (_mainLoop is { })
{
try
{
if (!_inputHandlerTokenSource.IsCancellationRequested)
{
_waitForProbe.Wait (_inputHandlerTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return;
}
finally
{
if (_waitForProbe.IsSet)
{
_waitForProbe.Reset ();
}
}
if (_inputHandlerTokenSource.IsCancellationRequested)
{
return;
}
if (_resultQueue.Count == 0)
{
_resultQueue.Enqueue (_netEvents.DequeueInput ());
}
try
{
while (_resultQueue.Peek () is null)
{
_resultQueue.Dequeue ();
}
if (_resultQueue.Count > 0)
{
_eventReady.Set ();
}
}
catch (InvalidOperationException)
{
// Ignore
}
}
}
}