瀏覽代碼

Split more WindowDriver classes and #nullable enable.

BDisp 8 月之前
父節點
當前提交
44d5997460

+ 179 - 0
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 ()); }
+}

+ 28 - 28
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 (

+ 37 - 455
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 ();
 
     /// <inheritdoc/>
-    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
         };
     }
 }
-
-/// <summary>
-///     Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
-///     only be used on Windows.
-/// </summary>
-/// <remarks>
-///     This implementation is used for WindowsDriver.
-/// </remarks>
-internal class WindowsMainLoop : IMainLoopDriver
-{
-    /// <summary>
-    ///     Invoked when the window is changed.
-    /// </summary>
-    public EventHandler<SizeChangedEventArgs> WinChanged;
-
-    private readonly ConsoleDriver _consoleDriver;
-    private readonly ManualResetEventSlim _eventReady = new (false);
-
-    // The records that we keep fetching
-    private readonly ConcurrentQueue<WindowsConsole.InputRecord []> _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 ()); }
-}

+ 248 - 0
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs

@@ -0,0 +1,248 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Mainloop intended to be used with the <see cref="WindowsDriver"/>, and can
+///     only be used on Windows.
+/// </summary>
+/// <remarks>
+///     This implementation is used for WindowsDriver.
+/// </remarks>
+internal class WindowsMainLoop : IMainLoopDriver
+{
+    /// <summary>
+    ///     Invoked when the window is changed.
+    /// </summary>
+    public EventHandler<SizeChangedEventArgs>? WinChanged;
+
+    private readonly ConsoleDriver _consoleDriver;
+    private readonly ManualResetEventSlim _eventReady = new (false);
+
+    // The records that we keep fetching
+    private readonly ConcurrentQueue<WindowsConsole.InputRecord []> _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
+}
+