diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs new file mode 100644 index 000000000..d38ffb408 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/ClipboardImpl.cs @@ -0,0 +1,179 @@ +#nullable enable +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Terminal.Gui; + +internal class WindowsClipboard : ClipboardBase +{ + private const uint CF_UNICODE_TEXT = 13; + + public override bool IsSupported { get; } = CheckClipboardIsAvailable (); + + private static bool CheckClipboardIsAvailable () + { + // Attempt to open the clipboard + if (OpenClipboard (nint.Zero)) + { + // Clipboard is available + // Close the clipboard after use + CloseClipboard (); + + return true; + } + // Clipboard is not available + return false; + } + + protected override string GetClipboardDataImpl () + { + try + { + if (!OpenClipboard (nint.Zero)) + { + return string.Empty; + } + + nint handle = GetClipboardData (CF_UNICODE_TEXT); + + if (handle == nint.Zero) + { + return string.Empty; + } + + nint pointer = nint.Zero; + + try + { + pointer = GlobalLock (handle); + + if (pointer == nint.Zero) + { + return string.Empty; + } + + int size = GlobalSize (handle); + var buff = new byte [size]; + + Marshal.Copy (pointer, buff, 0, size); + + return Encoding.Unicode.GetString (buff).TrimEnd ('\0'); + } + finally + { + if (pointer != nint.Zero) + { + GlobalUnlock (handle); + } + } + } + finally + { + CloseClipboard (); + } + } + + protected override void SetClipboardDataImpl (string text) + { + OpenClipboard (); + + EmptyClipboard (); + nint hGlobal = default; + + try + { + int bytes = (text.Length + 1) * 2; + hGlobal = Marshal.AllocHGlobal (bytes); + + if (hGlobal == default (nint)) + { + ThrowWin32 (); + } + + nint target = GlobalLock (hGlobal); + + if (target == default (nint)) + { + ThrowWin32 (); + } + + try + { + Marshal.Copy (text.ToCharArray (), 0, target, text.Length); + } + finally + { + GlobalUnlock (target); + } + + if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint)) + { + ThrowWin32 (); + } + + hGlobal = default (nint); + } + finally + { + if (hGlobal != default (nint)) + { + Marshal.FreeHGlobal (hGlobal); + } + + CloseClipboard (); + } + } + + [DllImport ("user32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool CloseClipboard (); + + [DllImport ("user32.dll")] + private static extern bool EmptyClipboard (); + + [DllImport ("user32.dll", SetLastError = true)] + private static extern nint GetClipboardData (uint uFormat); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint GlobalLock (nint hMem); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern int GlobalSize (nint handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool GlobalUnlock (nint hMem); + + [DllImport ("User32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool IsClipboardFormatAvailable (uint format); + + private void OpenClipboard () + { + var num = 10; + + while (true) + { + if (OpenClipboard (default (nint))) + { + break; + } + + if (--num == 0) + { + ThrowWin32 (); + } + + Thread.Sleep (100); + } + } + + [DllImport ("user32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool OpenClipboard (nint hWndNewOwner); + + [DllImport ("user32.dll", SetLastError = true)] + private static extern nint SetClipboardData (uint uFormat, nint data); + + private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); } +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 430085a2a..ff8d155b7 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -1,4 +1,4 @@ -// TODO: #nullable enable +#nullable enable using System.ComponentModel; using System.Runtime.InteropServices; using Terminal.Gui.ConsoleDrivers; @@ -7,7 +7,7 @@ namespace Terminal.Gui; internal class WindowsConsole { - internal WindowsMainLoop _mainLoop; + internal WindowsMainLoop? _mainLoop; public const int STD_OUTPUT_HANDLE = -11; public const int STD_INPUT_HANDLE = -10; @@ -34,7 +34,7 @@ internal class WindowsConsole ConsoleMode = newConsoleMode; } - private CharInfo [] _originalStdOutChars; + private CharInfo []? _originalStdOutChars; public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) { @@ -598,15 +598,15 @@ internal class WindowsConsole public readonly override string ToString () { - return EventType switch - { - EventType.Focus => FocusEvent.ToString (), - EventType.Key => KeyEvent.ToString (), - EventType.Menu => MenuEvent.ToString (), - EventType.Mouse => MouseEvent.ToString (), - EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), - _ => "Unknown event type: " + EventType - }; + return (EventType switch + { + EventType.Focus => FocusEvent.ToString (), + EventType.Key => KeyEvent.ToString (), + EventType.Menu => MenuEvent.ToString (), + EventType.Mouse => MouseEvent.ToString (), + EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), + _ => "Unknown event type: " + EventType + })!; } } @@ -866,7 +866,7 @@ internal class WindowsConsole internal static nint INVALID_HANDLE_VALUE = new (-1); [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleActiveScreenBuffer (nint Handle); + private static extern bool SetConsoleActiveScreenBuffer (nint handle); [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool GetNumberOfConsoleInputEvents (nint handle, out uint lpcNumberOfEvents); @@ -896,9 +896,9 @@ internal class WindowsConsole private int _retries; - public InputRecord [] ReadConsoleInput () + public InputRecord []? ReadConsoleInput () { - const int bufferSize = 1; + const int BUFFER_SIZE = 1; InputRecord inputRecord = default; uint numberEventsRead = 0; StringBuilder ansiSequence = new StringBuilder (); @@ -910,13 +910,13 @@ internal class WindowsConsole try { // Peek to check if there is any input available - if (PeekConsoleInput (_inputHandle, out _, bufferSize, out uint eventsRead) && eventsRead > 0) + if (PeekConsoleInput (_inputHandle, out _, BUFFER_SIZE, out uint eventsRead) && eventsRead > 0) { // Read the input since it is available ReadConsoleInput ( _inputHandle, out inputRecord, - bufferSize, + BUFFER_SIZE, out numberEventsRead); if (inputRecord.EventType == EventType.Key) @@ -931,7 +931,7 @@ internal class WindowsConsole if (inputChar == '\u001B') // Escape character { // Peek to check if there is any input available with key event and bKeyDown - if (PeekConsoleInput (_inputHandle, out InputRecord peekRecord, bufferSize, out eventsRead) && eventsRead > 0) + if (PeekConsoleInput (_inputHandle, out InputRecord peekRecord, BUFFER_SIZE, out eventsRead) && eventsRead > 0) { if (peekRecord is { EventType: EventType.Key, KeyEvent.bKeyDown: true }) { @@ -949,7 +949,7 @@ internal class WindowsConsole ansiSequence.Append (inputChar); // Check if the sequence has ended with an expected command terminator - if (_mainLoop.EscSeqRequests is { } && _mainLoop.EscSeqRequests.HasResponse (inputChar.ToString (), out AnsiEscapeSequenceRequestStatus seqReqStatus)) + if (_mainLoop?.EscSeqRequests is { } && _mainLoop.EscSeqRequests.HasResponse (inputChar.ToString (), out AnsiEscapeSequenceRequestStatus? seqReqStatus)) { // Finished reading the sequence and remove the enqueued request _mainLoop.EscSeqRequests.Remove (seqReqStatus); @@ -970,9 +970,9 @@ internal class WindowsConsole } } - if (readingSequence && !raisedResponse && AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 }) + if (readingSequence && !raisedResponse && AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && _mainLoop?.EscSeqRequests is { Statuses.Count: > 0 }) { - _mainLoop.EscSeqRequests.Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus seqReqStatus); + _mainLoop.EscSeqRequests.Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus? seqReqStatus); lock (seqReqStatus!.AnsiRequest._responseLock) { @@ -984,13 +984,13 @@ internal class WindowsConsole _retries = 0; } - else if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && _mainLoop.EscSeqRequests is { Statuses.Count: > 0 }) + else if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && _mainLoop?.EscSeqRequests is { Statuses.Count: > 0 }) { if (_retries > 1) { - if (_mainLoop.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response)) + if (_mainLoop.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? seqReqStatus) && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.Response)) { - lock (seqReqStatus!.AnsiRequest._responseLock) + lock (seqReqStatus.AnsiRequest._responseLock) { _mainLoop.EscSeqRequests.Statuses.TryDequeue (out _); @@ -1012,9 +1012,9 @@ internal class WindowsConsole _retries = 0; } - return numberEventsRead == 0 - ? null - : [inputRecord]; + return (numberEventsRead == 0 + ? null + : [inputRecord])!; } catch (Exception) { @@ -1096,7 +1096,7 @@ internal class WindowsConsole private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi); [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX ConsoleScreenBufferInfo); + private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo); [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool SetConsoleWindowInfo ( diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 9372fcb04..616dc9fb0 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -1,4 +1,4 @@ -// TODO: #nullable enable +#nullable enable // // WindowsDriver.cs: Windows specific driver // @@ -9,14 +9,13 @@ // 2) The values provided during Init (and the first WindowsConsole.EventType.WindowBufferSize) are not correct. // // If HACK_CHECK_WINCHANGED is defined then we ignore WindowsConsole.EventType.WindowBufferSize events -// and instead check the console size every 500ms in a thread in WidowsMainLoop. -// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using +// and instead check the console size every 500ms in a thread in WidowsMainLoop. +// As of Windows 11 23H2 25947.1000 and/or WT 1.19.2682 tearing no longer occurs when using // the WindowsConsole.EventType.WindowBufferSize event. However, on Init the window size is // still incorrect so we still need this hack. //#define HACK_CHECK_WINCHANGED -using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; @@ -35,7 +34,7 @@ internal class WindowsDriver : ConsoleDriver private bool _isOneFingerDoubleClicked; private WindowsConsole.ButtonState? _lastMouseButtonPressed; - private WindowsMainLoop _mainLoopDriver; + private WindowsMainLoop? _mainLoopDriver; private WindowsConsole.ExtendedCharInfo [] _outputBuffer; private Point? _point; private Point _pointMove; @@ -45,7 +44,7 @@ internal class WindowsDriver : ConsoleDriver { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - WinConsole = new WindowsConsole (); + WinConsole = new (); // otherwise we're probably running in unit tests Clipboard = new WindowsClipboard (); @@ -68,7 +67,7 @@ internal class WindowsDriver : ConsoleDriver public override bool SupportsTrueColor => RunningUnitTests || (Environment.OSVersion.Version.Build >= 14931 && _isWindowsTerminal); - public WindowsConsole WinConsole { get; private set; } + public WindowsConsole? WinConsole { get; private set; } public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) { @@ -202,7 +201,7 @@ internal class WindowsDriver : ConsoleDriver private readonly CancellationTokenSource _ansiResponseTokenSource = new (); /// - public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest) + public override string? WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest) { if (_mainLoopDriver is null) { @@ -242,12 +241,12 @@ internal class WindowsDriver : ConsoleDriver { _mainLoopDriver._forceRead = false; - if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus request)) + if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus? request)) { if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0 && string.IsNullOrEmpty (request.AnsiRequest.Response)) { - lock (request!.AnsiRequest._responseLock) + lock (request.AnsiRequest._responseLock) { // Bad request or no response at all _mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _); @@ -404,7 +403,7 @@ internal class WindowsDriver : ConsoleDriver for (var row = 0; row < Rows; row++) { - if (!_dirtyLines [row]) + if (!_dirtyLines! [row]) { continue; } @@ -414,7 +413,7 @@ internal class WindowsDriver : ConsoleDriver for (var col = 0; col < Cols; col++) { int position = row * Cols + col; - _outputBuffer [position].Attribute = Contents [row, col].Attribute.GetValueOrDefault (); + _outputBuffer [position].Attribute = Contents! [row, col].Attribute.GetValueOrDefault (); if (Contents [row, col].IsDirty == false) { @@ -504,7 +503,7 @@ internal class WindowsDriver : ConsoleDriver { // BUGBUG: The results from GetConsoleOutputWindow are incorrect when called from Init. // Our thread in WindowsMainLoop.CheckWin will get the correct results. See #if HACK_CHECK_WINCHANGED - Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos); + Size winSize = WinConsole.GetConsoleOutputWindow (out Point _); Cols = winSize.Width; Rows = winSize.Height; } @@ -592,7 +591,7 @@ internal class WindowsDriver : ConsoleDriver case WindowsConsole.EventType.Mouse: MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); - if (me is null || me.Flags == MouseFlags.None) + if (me.Flags == MouseFlags.None) { break; } @@ -717,7 +716,7 @@ internal class WindowsDriver : ConsoleDriver if (mapResult == 0) { // There is no mapping - this should not happen - Debug.Assert (mapResult != 0, $@"Unable to map the virtual key code {keyInfo.Key}."); + Debug.Assert (true, $@"Unable to map the virtual key code {keyInfo.Key}."); return KeyCode.Null; } @@ -727,13 +726,13 @@ internal class WindowsDriver : ConsoleDriver if (keyInfo.KeyChar == 0) { - // If the keyChar is 0, keyInfo.Key value is not a printable character. + // If the keyChar is 0, keyInfo.Key value is not a printable character. - // Dead keys (diacritics) are indicated by setting the top bit of the return value. + // Dead keys (diacritics) are indicated by setting the top bit of the return value. if ((mapResult & 0x80000000) != 0) { // Dead key (e.g. Oem2 '~'/'^' on POR keyboard) - // Option 1: Throw it out. + // Option 1: Throw it out. // - Apps will never see the dead keys // - If user presses a key that can be combined with the dead key ('a'), the right thing happens (app will see '�'). // - NOTE: With Dead Keys, KeyDown != KeyUp. The KeyUp event will have just the base char ('a'). @@ -754,7 +753,7 @@ internal class WindowsDriver : ConsoleDriver if (keyInfo.Modifiers != 0) { // These Oem keys have well-defined chars. We ensure the representative char is used. - // If we don't do this, then on some keyboard layouts the wrong char is + // If we don't do this, then on some keyboard layouts the wrong char is // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important // for key persistence ("Ctrl++" vs. "Ctrl+="). mappedChar = keyInfo.Key switch @@ -925,25 +924,25 @@ internal class WindowsDriver : ConsoleDriver { // When a user presses-and-holds, start generating pressed events every `startDelay` // After `iterationsUntilFast` iterations, speed them up to `fastDelay` ms - const int startDelay = 500; - const int iterationsUntilFast = 4; - const int fastDelay = 50; + const int START_DELAY = 500; + const int ITERATIONS_UNTIL_FAST = 4; + const int FAST_DELAY = 50; int iterations = 0; - int delay = startDelay; + int delay = START_DELAY; while (_isButtonPressed) { // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - View view = Application.WantContinuousButtonPressedView; + View? view = Application.WantContinuousButtonPressedView; if (view is null) { break; } - if (iterations++ >= iterationsUntilFast) + if (iterations++ >= ITERATIONS_UNTIL_FAST) { - delay = fastDelay; + delay = FAST_DELAY; } await Task.Delay (delay); @@ -1012,13 +1011,13 @@ internal class WindowsDriver : ConsoleDriver if (_isButtonDoubleClicked || _isOneFingerDoubleClicked) { // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop.AddIdle ( - () => - { - Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); + Application.MainLoop!.AddIdle ( + () => + { + Task.Run (async () => await ProcessButtonDoubleClickedAsync ()); - return false; - }); + return false; + }); } // The ButtonState member of the MouseEvent structure has bit corresponding to each mouse button. @@ -1084,13 +1083,13 @@ internal class WindowsDriver : ConsoleDriver if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.MainLoop.AddIdle ( - () => - { - Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); + Application.MainLoop!.AddIdle ( + () => + { + Task.Run (async () => await ProcessContinuousButtonPressedAsync (mouseFlag)); - return false; - }); + return false; + }); } } else if (_lastMouseButtonPressed != null @@ -1254,420 +1253,3 @@ internal class WindowsDriver : ConsoleDriver }; } } - -/// -/// 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 ConcurrentQueue _resultQueue = new (); - internal readonly ManualResetEventSlim _waitForProbe = new (false); - private readonly WindowsConsole _winConsole; - private CancellationTokenSource _eventReadyTokenSource = new (); - private readonly CancellationTokenSource _inputHandlerTokenSource = new (); - private MainLoop _mainLoop; - - public WindowsMainLoop (ConsoleDriver consoleDriver = null) - { - _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 () - { - _waitForProbe.Set (); -#if HACK_CHECK_WINCHANGED - _winChange.Set (); -#endif - 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) - { -#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 (); - - return true; - } - - void IMainLoopDriver.Iteration () - { - while (_resultQueue.Count > 0) - { - if (_resultQueue.TryDequeue (out WindowsConsole.InputRecord [] inputRecords)) - { - if (inputRecords is { Length: > 0 }) - { - ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]); - } - } - } -#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."); - } - } - - _waitForProbe?.Dispose (); - - _resultQueue?.Clear (); - - _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 && !_forceRead) - { - _waitForProbe.Wait (_inputHandlerTokenSource.Token); - } - } - catch (OperationCanceledException) - { - // Wakes the _waitForProbe if it's waiting - _waitForProbe.Set (); - - return; - } - finally - { - // If IsCancellationRequested is true the code after - // the `finally` block will not be executed. - if (!_inputHandlerTokenSource.IsCancellationRequested) - { - _waitForProbe.Reset (); - } - } - - if (_resultQueue?.Count == 0 || _forceRead) - { - while (!_inputHandlerTokenSource.IsCancellationRequested) - { - WindowsConsole.InputRecord [] inpRec = _winConsole.ReadConsoleInput (); - - if (inpRec is { }) - { - _resultQueue!.Enqueue (inpRec); - - break; - } - - if (!_forceRead) - { - try - { - Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token); - } - catch (OperationCanceledException) - { } - } - } - } - - _eventReady.Set (); - } - } - -#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 -} - -internal class WindowsClipboard : ClipboardBase -{ - private const uint CF_UNICODE_TEXT = 13; - - public override bool IsSupported { get; } = CheckClipboardIsAvailable (); - - private static bool CheckClipboardIsAvailable () - { - // Attempt to open the clipboard - if (OpenClipboard (nint.Zero)) - { - // Clipboard is available - // Close the clipboard after use - CloseClipboard (); - - return true; - } - // Clipboard is not available - return false; - } - - protected override string GetClipboardDataImpl () - { - try - { - if (!OpenClipboard (nint.Zero)) - { - return string.Empty; - } - - nint handle = GetClipboardData (CF_UNICODE_TEXT); - - if (handle == nint.Zero) - { - return string.Empty; - } - - nint pointer = nint.Zero; - - try - { - pointer = GlobalLock (handle); - - if (pointer == nint.Zero) - { - return string.Empty; - } - - int size = GlobalSize (handle); - var buff = new byte [size]; - - Marshal.Copy (pointer, buff, 0, size); - - return Encoding.Unicode.GetString (buff).TrimEnd ('\0'); - } - finally - { - if (pointer != nint.Zero) - { - GlobalUnlock (handle); - } - } - } - finally - { - CloseClipboard (); - } - } - - protected override void SetClipboardDataImpl (string text) - { - OpenClipboard (); - - EmptyClipboard (); - nint hGlobal = default; - - try - { - int bytes = (text.Length + 1) * 2; - hGlobal = Marshal.AllocHGlobal (bytes); - - if (hGlobal == default (nint)) - { - ThrowWin32 (); - } - - nint target = GlobalLock (hGlobal); - - if (target == default (nint)) - { - ThrowWin32 (); - } - - try - { - Marshal.Copy (text.ToCharArray (), 0, target, text.Length); - } - finally - { - GlobalUnlock (target); - } - - if (SetClipboardData (CF_UNICODE_TEXT, hGlobal) == default (nint)) - { - ThrowWin32 (); - } - - hGlobal = default (nint); - } - finally - { - if (hGlobal != default (nint)) - { - Marshal.FreeHGlobal (hGlobal); - } - - CloseClipboard (); - } - } - - [DllImport ("user32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool CloseClipboard (); - - [DllImport ("user32.dll")] - private static extern bool EmptyClipboard (); - - [DllImport ("user32.dll", SetLastError = true)] - private static extern nint GetClipboardData (uint uFormat); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint GlobalLock (nint hMem); - - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern int GlobalSize (nint handle); - - [DllImport ("kernel32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool GlobalUnlock (nint hMem); - - [DllImport ("User32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool IsClipboardFormatAvailable (uint format); - - private void OpenClipboard () - { - var num = 10; - - while (true) - { - if (OpenClipboard (default (nint))) - { - break; - } - - if (--num == 0) - { - ThrowWin32 (); - } - - Thread.Sleep (100); - } - } - - [DllImport ("user32.dll", SetLastError = true)] - [return: MarshalAs (UnmanagedType.Bool)] - private static extern bool OpenClipboard (nint hWndNewOwner); - - [DllImport ("user32.dll", SetLastError = true)] - private static extern nint SetClipboardData (uint uFormat, nint data); - - private void ThrowWin32 () { throw new Win32Exception (Marshal.GetLastWin32Error ()); } -} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs new file mode 100644 index 000000000..33aca9704 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs @@ -0,0 +1,248 @@ +#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 ConcurrentQueue _resultQueue = new (); + internal readonly ManualResetEventSlim _waitForProbe = new (false); + 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 () + { + _waitForProbe.Set (); +#if HACK_CHECK_WINCHANGED + _winChange.Set (); +#endif + 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) + { +#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 (); + + return true; + } + + void IMainLoopDriver.Iteration () + { + while (_resultQueue.Count > 0) + { + if (_resultQueue.TryDequeue (out WindowsConsole.InputRecord []? inputRecords)) + { + if (inputRecords is { Length: > 0 }) + { + ((WindowsDriver)_consoleDriver).ProcessInput (inputRecords [0]); + } + } + } +#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."); + } + } + + _waitForProbe.Dispose (); + + _resultQueue.Clear (); + + _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 && !_forceRead) + { + _waitForProbe.Wait (_inputHandlerTokenSource.Token); + } + } + catch (OperationCanceledException) + { + // Wakes the _waitForProbe if it's waiting + _waitForProbe.Set (); + + return; + } + finally + { + // If IsCancellationRequested is true the code after + // the `finally` block will not be executed. + if (!_inputHandlerTokenSource.IsCancellationRequested) + { + _waitForProbe.Reset (); + } + } + + if (_resultQueue?.Count == 0 || _forceRead) + { + while (!_inputHandlerTokenSource.IsCancellationRequested) + { + WindowsConsole.InputRecord [] inpRec = _winConsole.ReadConsoleInput (); + + if (inpRec is { }) + { + _resultQueue!.Enqueue (inpRec); + + break; + } + + if (!_forceRead) + { + try + { + Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token); + } + catch (OperationCanceledException) + { } + } + } + } + + _eventReady.Set (); + } + } + +#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 +} +