// // 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 _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 (); } } /// Enqueue a window size event if the window size has changed. /// /// /// /// /// 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, 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 }; } /// Prints a ConsoleKeyInfoEx structure /// /// 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 ConsoleColorValues = new ( Enum.GetValues (typeof (ConsoleColor)) .OfType () .Select (c => (int)c) ); // Dictionary for mapping ConsoleColor values to the values used by System.Net.Console. private static readonly Dictionary 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; } ///// ///// 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. ///// //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 } /// /// 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. /// /// This implementation is used for NetDriver. internal class NetMainLoop : IMainLoopDriver { internal NetEvents _netEvents; /// Invoked when a Key is pressed. internal Action ProcessInput; private readonly ManualResetEventSlim _eventReady = new (false); private readonly CancellationTokenSource _inputHandlerTokenSource = new (); private readonly Queue _resultQueue = new (); private readonly ManualResetEventSlim _waitForProbe = new (false); private CancellationTokenSource _eventReadyTokenSource = new (); private MainLoop _mainLoop; /// Initializes the class with the console driver. /// Passing a consoleDriver is provided to capture windows resizing. /// The console driver used by this Net main loop. /// 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 } } } }