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
+}
+