diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index d653780aa..9d145b7a7 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1804,7 +1804,7 @@ public static class EscSeqUtils /// /// The terminal reply to . ESC [ ? (y) ; (x) R /// - public static readonly string CSI_RequestCursorPositionReport_Terminator = "R"; + public const string CSI_RequestCursorPositionReport_Terminator = "R"; /// /// ESC [ 0 c - Send Device Attributes (Primary DA) @@ -1855,13 +1855,13 @@ public static class EscSeqUtils /// /// The terminator indicating a reply to : ESC [ 8 ; height ; width t /// - public static readonly string CSI_ReportTerminalSizeInChars_Terminator = "t"; + public const string CSI_ReportTerminalSizeInChars_Terminator = "t"; /// /// The value of the response to indicating value 1 and 2 are the terminal /// size in chars. /// - public static readonly string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; + public const string CSI_ReportTerminalSizeInChars_ResponseValue = "8"; #endregion } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 27c11cedb..aed22b6df 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -40,6 +40,9 @@ public class FakeDriver : ConsoleDriver public static Behaviors FakeBehaviors = new (); public override bool SupportsTrueColor => false; + /// + public override void WriteRaw (string ansi) { throw new NotImplementedException (); } + public FakeDriver () { Cols = FakeConsole.WindowWidth = FakeConsole.BufferWidth = FakeConsole.WIDTH; diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs index 62c6db94b..dbcfb72f4 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -3,7 +3,9 @@ // NetDriver.cs: The System.Console-based .NET driver, works on Windows and Unix, but is not particularly efficient. // +using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using static Terminal.Gui.NetEvents; @@ -11,9 +13,11 @@ namespace Terminal.Gui; internal class NetDriver : ConsoleDriver { + public bool IsWinPlatform { get; private set; } public NetWinVTConsole? NetWinConsole { get; private set; } + public override void Suspend () { if (Environment.OSVersion.Platform != PlatformID.Unix) @@ -132,33 +136,30 @@ internal class NetDriver : ConsoleDriver { output.Append ( EscSeqUtils.CSI_SetGraphicsRendition ( - MapColors ( - (ConsoleColor)attr.Background - .GetClosestNamedColor16 (), - false - ), - MapColors ( - (ConsoleColor)attr.Foreground - .GetClosestNamedColor16 ()) - ) + MapColors ( + (ConsoleColor)attr.Background.GetClosestNamedColor16 (), + false + ), + MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor16 ()) + ) ); } else { output.Append ( EscSeqUtils.CSI_SetForegroundColorRGB ( - attr.Foreground.R, - attr.Foreground.G, - attr.Foreground.B - ) + attr.Foreground.R, + attr.Foreground.G, + attr.Foreground.B + ) ); output.Append ( EscSeqUtils.CSI_SetBackgroundColorRGB ( - attr.Background.R, - attr.Background.G, - attr.Background.B - ) + attr.Background.R, + attr.Background.G, + attr.Background.B + ) ); } } @@ -221,10 +222,17 @@ internal class NetDriver : ConsoleDriver return updated; } - #region Init/End/MainLoop - + /// + internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; internal NetMainLoop? _mainLoopDriver; + /// + internal override void RawWrite (string str) + { + Console.Write (str); + } + + public override MainLoop Init () { @@ -236,7 +244,7 @@ internal class NetDriver : ConsoleDriver try { - NetWinConsole = new (); + NetWinConsole = new NetWinVTConsole (); } catch (ApplicationException) { @@ -295,6 +303,7 @@ internal class NetDriver : ConsoleDriver _mainLoopDriver.ProcessInput = ProcessInput; return new (_mainLoopDriver); + return new MainLoop (_mainLoopDriver); } private void ProcessInput (InputResult inputEvent) @@ -323,7 +332,6 @@ internal class NetDriver : ConsoleDriver break; case EventType.Mouse: MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); - //Debug.WriteLine ($"NetDriver: ({me.X},{me.Y}) - {me.Flags}"); OnMouseEvent (me); @@ -334,7 +342,7 @@ internal class NetDriver : ConsoleDriver Left = 0; Cols = inputEvent.WindowSizeEvent.Size.Width; Rows = Math.Max (inputEvent.WindowSizeEvent.Size.Height, 0); - + ; ResizeScreen (); ClearContents (); _winSizeChanging = false; @@ -349,7 +357,6 @@ internal class NetDriver : ConsoleDriver throw new ArgumentOutOfRangeException (); } } - public override void End () { if (IsWinPlatform) @@ -374,6 +381,9 @@ internal class NetDriver : ConsoleDriver #endregion Init/End/MainLoop + + + #region Color Handling public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix @@ -787,4 +797,4 @@ internal class NetDriver : ConsoleDriver } #endregion Low-Level DotNet tuff -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs index 9ba56d673..d7e0113cc 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs @@ -1,238 +1,150 @@ #nullable enable +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui; internal class NetEvents : IDisposable { - private readonly ManualResetEventSlim _inputReady = new (false); - private CancellationTokenSource? _inputReadyCancellationTokenSource; - private readonly Queue _inputQueue = new (); - private readonly IConsoleDriver _consoleDriver; - private ConsoleKeyInfo []? _cki; - private bool _isEscSeq; -#if PROCESS_REQUEST - bool _neededProcessRequest; -#endif - public NetEvents (IConsoleDriver consoleDriver) + private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource (); + + //CancellationTokenSource _waitForStartCancellationTokenSource; + private readonly ManualResetEventSlim _winChange = new (false); + private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); + private readonly ConsoleDriver _consoleDriver; + + public AnsiResponseParser Parser { get; private set; } = new (); + + public NetEvents (ConsoleDriver consoleDriver) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); - _inputReadyCancellationTokenSource = new (); - Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); - - Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token); - } - - public InputResult? DequeueInput () - { - while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false }) + Task.Run (() => { try { - if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) - { - if (_inputQueue.Count == 0) - { - _inputReady.Wait (_inputReadyCancellationTokenSource.Token); - } - } + ProcessInputQueue (); + } + catch (OperationCanceledException) + { } + }, _netEventsDisposed.Token); - if (_inputQueue.Count > 0) + Task.Run (() => { + try + { + CheckWindowSizeChange (); + } + catch (OperationCanceledException) + { } + }, _netEventsDisposed.Token); + + Parser.UnexpectedResponseHandler = ProcessRequestResponse; + } + + + public InputResult? DequeueInput () + { + while (!_netEventsDisposed.Token.IsCancellationRequested) + { + _winChange.Set (); + + try + { + if (_inputQueue.TryTake (out var item, -1, _netEventsDisposed.Token)) { - return _inputQueue.Dequeue (); + return item; } } catch (OperationCanceledException) { return null; } - finally - { - if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) - { - _inputReady.Reset (); - } - } -#if PROCESS_REQUEST - _neededProcessRequest = false; -#endif } return null; } - private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true) + private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true) { - while (!cancellationToken.IsCancellationRequested) + // if there is a key available, return it without waiting + // (or dispatching work to the thread queue) + if (Console.KeyAvailable) { - // if there is a key available, return it without waiting - // (or dispatching work to the thread queue) + return Console.ReadKey (intercept); + } + + while (!_netEventsDisposed.IsCancellationRequested) + { + Task.Delay (100, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token); + + foreach (var k in ShouldRelease ()) + { + ProcessMapConsoleKeyInfo (k); + } + if (Console.KeyAvailable) { return Console.ReadKey (intercept); } - - // The delay must be here because it may have a request response after a while - // In WSL it takes longer for keys to be available. - Task.Delay (100, cancellationToken).Wait (cancellationToken); } - cancellationToken.ThrowIfCancellationRequested (); + _netEventsDisposed.Token.ThrowIfCancellationRequested (); return default (ConsoleKeyInfo); } + public IEnumerable ShouldRelease () + { + if (Parser.State == AnsiResponseParserState.ExpectingBracket && + DateTime.Now - Parser.StateChangedAt > _consoleDriver.EscTimeout) + { + return Parser.Release ().Select (o => o.Item2); + } + + return []; + } + private void ProcessInputQueue () { - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { - try + if (_inputQueue.Count == 0) { - ConsoleKey key = 0; - ConsoleModifiers mod = 0; - ConsoleKeyInfo newConsoleKeyInfo = default; - - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { ConsoleKeyInfo consoleKeyInfo; - try + consoleKeyInfo = ReadConsoleKeyInfo (); + + // Parse + foreach (var k in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { - consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token); + ProcessMapConsoleKeyInfo (k.Item2); } - catch (OperationCanceledException) - { - return; - } - - var ckiAlreadyResized = false; - - if (EscSeqUtils.IncompleteCkInfos is { }) - { - ckiAlreadyResized = true; - - _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki); - _cki = EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki); - EscSeqUtils.IncompleteCkInfos = null; - - if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B') - { - _isEscSeq = true; - } - } - - 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 ( - (char)KeyCode.Esc, - 0, - false, - false, - false - ), - _cki - ); - } - - _isEscSeq = true; - - if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space) - || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127) - || (_cki is { } - && char.IsLetter (_cki [^1].KeyChar) - && char.IsLower (consoleKeyInfo.KeyChar) - && char.IsLetter (consoleKeyInfo.KeyChar)) - || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetterOrDigit (consoleKeyInfo.KeyChar)) - || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar)) - || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar))) - { - ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); - _cki = null; - _isEscSeq = false; - - ProcessMapConsoleKeyInfo (consoleKeyInfo); - } - else - { - newConsoleKeyInfo = consoleKeyInfo; - - if (!ckiAlreadyResized) - { - _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; - } - - if (_inputQueue.Count > 0) - { - _inputReady.Set (); } } - catch (OperationCanceledException) - { - return; - } } + } - void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) - { - _inputQueue.Enqueue ( - new () - { - EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) - } - ); - _isEscSeq = false; - } + void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + _inputQueue.Add ( + new InputResult + { + EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo) + } + ); } private void CheckWindowSizeChange () { - void RequestWindowSize (CancellationToken cancellationToken) + void RequestWindowSize () { - while (!cancellationToken.IsCancellationRequested) + while (!_netEventsDisposed.IsCancellationRequested) { // Wait for a while then check if screen has changed sizes - Task.Delay (500, cancellationToken).Wait (cancellationToken); + Task.Delay (500, _netEventsDisposed.Token).Wait (_netEventsDisposed.Token); int buffHeight, buffWidth; @@ -258,19 +170,17 @@ internal class NetEvents : IDisposable } } - cancellationToken.ThrowIfCancellationRequested (); + _netEventsDisposed.Token.ThrowIfCancellationRequested (); } - while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) + while (!_netEventsDisposed.IsCancellationRequested) { try { - RequestWindowSize (_inputReadyCancellationTokenSource.Token); + _winChange.Wait (_netEventsDisposed.Token); + _winChange.Reset (); - if (_inputQueue.Count > 0) - { - _inputReady.Set (); - } + RequestWindowSize (); } catch (OperationCanceledException) { @@ -295,16 +205,29 @@ internal class NetEvents : IDisposable int w = Math.Max (winWidth, 0); int h = Math.Max (winHeight, 0); - _inputQueue.Enqueue ( - new () + _inputQueue.Add ( + new InputResult { - EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (w, h) } + EventType = EventType.WindowSize, WindowSizeEvent = new WindowSizeEvent { Size = new (w, h) } } ); return true; } + private bool ProcessRequestResponse (IEnumerable> obj) + { + // Added for signature compatibility with existing method, not sure what they are even for. + ConsoleKeyInfo newConsoleKeyInfo = default; + ConsoleKey key = default; + ConsoleModifiers mod = default; + + ProcessRequestResponse (ref newConsoleKeyInfo, ref key, obj.Select (v => v.Item2).ToArray (), ref mod); + + // Handled + return true; + } + // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event) private void ProcessRequestResponse ( ref ConsoleKeyInfo newConsoleKeyInfo, @@ -313,22 +236,23 @@ internal class NetEvents : IDisposable ref ConsoleModifiers mod ) { + // isMouse is true if it's CSI<, false otherwise EscSeqUtils.DecodeEscSeq ( - 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) - ); + 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) { @@ -347,10 +271,7 @@ internal class NetEvents : IDisposable return; } - if (newConsoleKeyInfo != default) - { - HandleKeyboardEvent (newConsoleKeyInfo); - } + HandleKeyboardEvent (newConsoleKeyInfo); } [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] @@ -491,51 +412,53 @@ internal class NetEvents : IDisposable private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating) { - if (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. - // The observation is correct because the response isn't immediate and this is useless - EscSeqUtils.CSI_RequestCursorPositionReport_Terminator) - { - var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 }; + 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 }; + 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; - } - } - else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator) - { - if (values [0] == 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) - ); - } - else - { + _inputQueue.Add ( + 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); - } - } - else - { - EnqueueRequestResponseEvent (c1Control, code, values, terminating); - } - _inputReady.Set (); + break; + } } private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating) @@ -543,7 +466,7 @@ internal class NetEvents : IDisposable var eventType = EventType.RequestResponse; var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) }; - _inputQueue.Enqueue ( + _inputQueue.Add ( new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv } ); } @@ -552,8 +475,8 @@ internal class NetEvents : IDisposable { var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState }; - _inputQueue.Enqueue ( - new () { EventType = EventType.Mouse, MouseEvent = mouseEvent } + _inputQueue.Add ( + new InputResult { EventType = EventType.Mouse, MouseEvent = mouseEvent } ); } @@ -634,15 +557,15 @@ internal class NetEvents : IDisposable public readonly override string ToString () { - return (EventType switch - { - EventType.Key => ToString (ConsoleKeyInfo), - EventType.Mouse => MouseEvent.ToString (), + return EventType switch + { + EventType.Key => ToString (ConsoleKeyInfo), + EventType.Mouse => MouseEvent.ToString (), - //EventType.WindowSize => WindowSize.ToString (), - //EventType.RequestResponse => RequestResponse.ToString (), - _ => "Unknown event type: " + EventType - })!; + //EventType.WindowSize => WindowSize.ToString (), + //EventType.RequestResponse => RequestResponse.ToString (), + _ => "Unknown event type: " + EventType + }; } /// Prints a ConsoleKeyInfoEx structure @@ -667,16 +590,13 @@ internal class NetEvents : IDisposable { var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki }; - _inputQueue.Enqueue (inputResult); + _inputQueue.Add (inputResult); } public void Dispose () { - _inputReadyCancellationTokenSource?.Cancel (); - _inputReadyCancellationTokenSource?.Dispose (); - _inputReadyCancellationTokenSource = null; - - _inputReady.Dispose (); + _netEventsDisposed?.Cancel (); + _netEventsDisposed?.Dispose (); try { @@ -692,4 +612,4 @@ internal class NetEvents : IDisposable // Ignore - Console input has already been closed } } -} +} \ No newline at end of file