#nullable enable using System.Collections.Concurrent; namespace Terminal.Gui; /// /// Mainloop intended to be used with the , and can /// only be used on Windows. /// /// /// This implementation is used for WindowsDriver. /// internal class WindowsMainLoop : IMainLoopDriver { /// /// Invoked when the window is changed. /// public EventHandler? WinChanged; private readonly ConsoleDriver _consoleDriver; private readonly ManualResetEventSlim _eventReady = new (false); // The records that we keep fetching private readonly BlockingCollection _resultQueue = new (new ConcurrentQueue ()); private readonly WindowsConsole? _winConsole; private CancellationTokenSource _eventReadyTokenSource = new (); private readonly CancellationTokenSource _inputHandlerTokenSource = new (); private MainLoop? _mainLoop; public WindowsMainLoop (ConsoleDriver consoleDriver) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); if (!ConsoleDriver.RunningUnitTests) { _winConsole = ((WindowsDriver)consoleDriver).WinConsole; _winConsole!._mainLoop = this; } } public AnsiEscapeSequenceRequests EscSeqRequests { get; } = new (); void IMainLoopDriver.Setup (MainLoop mainLoop) { _mainLoop = mainLoop; if (ConsoleDriver.RunningUnitTests) { return; } Task.Run (WindowsInputHandler, _inputHandlerTokenSource.Token); #if HACK_CHECK_WINCHANGED Task.Run (CheckWinChange); #endif } void IMainLoopDriver.Wakeup () { _eventReady.Set (); } bool IMainLoopDriver.EventsPending () { #if HACK_CHECK_WINCHANGED _winChange.Set (); #endif if (_resultQueue.Count > 0 || _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) { #if HACK_CHECK_WINCHANGED return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged; #else return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); #endif } _eventReadyTokenSource.Dispose (); _eventReadyTokenSource = new CancellationTokenSource (); // If cancellation was requested then always return true return true; } void IMainLoopDriver.Iteration () { while (_resultQueue.Count > 0) { if (_resultQueue.TryTake (out WindowsConsole.InputRecord dequeueResult)) { ((WindowsDriver)_consoleDriver).ProcessInput (dequeueResult); } } #if HACK_CHECK_WINCHANGED if (_winChanged) { _winChanged = false; WinChanged?.Invoke (this, new SizeChangedEventArgs (_windowSize)); } #endif } void IMainLoopDriver.TearDown () { _inputHandlerTokenSource.Cancel (); _inputHandlerTokenSource.Dispose (); if (_winConsole is { }) { var numOfEvents = _winConsole.GetNumberOfConsoleInputEvents (); if (numOfEvents > 0) { _winConsole.FlushConsoleInputBuffer (); //Debug.WriteLine ($"Flushed {numOfEvents} events."); } } _resultQueue.Dispose (); _eventReadyTokenSource.Cancel (); _eventReadyTokenSource.Dispose (); _eventReady.Dispose (); #if HACK_CHECK_WINCHANGED _winChange?.Dispose (); #endif _mainLoop = null; } internal bool _forceRead; private void WindowsInputHandler () { while (_mainLoop is { }) { try { if (_inputHandlerTokenSource.IsCancellationRequested) { return; } if (_resultQueue?.Count == 0 || _forceRead) { WindowsConsole.InputRecord? result = _winConsole!.DequeueInput (); if (result.HasValue) { _resultQueue!.Add (result.Value); } } if (!_inputHandlerTokenSource.IsCancellationRequested && _resultQueue?.Count > 0) { _eventReady.Set (); } } catch (OperationCanceledException) { return; } } } #if HACK_CHECK_WINCHANGED private readonly ManualResetEventSlim _winChange = new (false); private bool _winChanged; private Size _windowSize; private void CheckWinChange () { while (_mainLoop is { }) { _winChange.Wait (); _winChange.Reset (); // Check if the window size changed every half second. // We do this to minimize the weird tearing seen on Windows when resizing the console while (_mainLoop is { }) { Task.Delay (500).Wait (); _windowSize = _winConsole.GetConsoleBufferWindow (out _); if (_windowSize != Size.Empty && (_windowSize.Width != _consoleDriver.Cols || _windowSize.Height != _consoleDriver.Rows)) { break; } } _winChanged = true; _eventReady.Set (); } } #endif }