Browse Source

Refactoring a lot CursesDriver.

BDisp 9 months ago
parent
commit
de45b4bf39

+ 94 - 371
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -17,7 +17,6 @@ internal class CursesDriver : ConsoleDriver
     private CursorVisibility? _initialCursorVisibility;
     private MouseFlags _lastMouseFlags;
     private UnixMainLoop _mainLoopDriver;
-    private object _processInputToken;
 
     public override int Cols
     {
@@ -42,7 +41,21 @@ internal class CursesDriver : ConsoleDriver
     public override bool SupportsTrueColor => true;
 
     /// <inheritdoc/>
-    public override bool EnsureCursorVisibility () { return false; }
+    public override bool EnsureCursorVisibility ()
+    {
+        if (!(Col >= 0 && Row >= 0 && Col < Cols && Row < Rows))
+        {
+            GetCursorVisibility (out CursorVisibility cursorVisibility);
+            _currentCursorVisibility = cursorVisibility;
+            SetCursorVisibility (CursorVisibility.Invisible);
+
+            return false;
+        }
+
+        SetCursorVisibility (_currentCursorVisibility ?? CursorVisibility.Default);
+
+        return _currentCursorVisibility == CursorVisibility.Default;
+    }
 
     /// <inheritdoc/>
     public override bool GetCursorVisibility (out CursorVisibility visibility)
@@ -193,6 +206,9 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
+    private readonly ManualResetEventSlim _waitAnsiResponse = new (false);
+    private readonly CancellationTokenSource _ansiResponseTokenSource = new ();
+
     /// <inheritdoc />
     public override string WriteAnsiRequest (AnsiEscapeSequenceRequest ansiRequest)
     {
@@ -201,33 +217,61 @@ internal class CursesDriver : ConsoleDriver
             return string.Empty;
         }
 
-        while (Console.KeyAvailable)
+        var response = string.Empty;
+
+        try
         {
-            _mainLoopDriver._forceRead = true;
+            lock (ansiRequest._responseLock)
+            {
+                ansiRequest.ResponseFromInput += (s, e) =>
+                                                 {
+                                                     Debug.Assert (s == ansiRequest);
 
-            _mainLoopDriver._waitForInput.Set ();
-            _mainLoopDriver._waitForInput.Reset ();
-        }
+                                                     ansiRequest.Response = response = e;
 
-        _mainLoopDriver._forceRead = false;
-        _mainLoopDriver._suspendRead = true;
+                                                     _waitAnsiResponse.Set ();
+                                                 };
 
-        try
-        {
-            Console.Out.Write (ansiRequest.Request);
-            Console.Out.Flush (); // Ensure the request is sent
-            Task.Delay (100).Wait (); // Allow time for the terminal to respond
+                _mainLoopDriver.EscSeqRequests.Add (ansiRequest, this);
 
-            return ReadAnsiResponseDefault (ansiRequest);
+                _mainLoopDriver._forceRead = true;
+            }
+
+            if (!_ansiResponseTokenSource.IsCancellationRequested)
+            {
+                _mainLoopDriver._waitForInput.Set ();
+
+                _waitAnsiResponse.Wait (_ansiResponseTokenSource.Token);
+            }
         }
-        catch (Exception)
+        catch (OperationCanceledException)
         {
             return string.Empty;
         }
         finally
         {
-            _mainLoopDriver._suspendRead = false;
+            _mainLoopDriver._forceRead = false;
+
+            if (_mainLoopDriver.EscSeqRequests.Statuses.TryPeek (out EscSeqReqStatus request))
+            {
+                if (_mainLoopDriver.EscSeqRequests.Statuses.Count > 0
+                    && string.IsNullOrEmpty (request.AnsiRequest.Response))
+                {
+                    // Bad request or no response at all
+                    _mainLoopDriver.EscSeqRequests.Statuses.TryDequeue (out _);
+                }
+            }
+
+            _waitAnsiResponse.Reset ();
         }
+
+        return response;
+    }
+
+    /// <inheritdoc />
+    public override void WriteRaw (string ansi)
+    {
+        _mainLoopDriver.WriteRaw (ansi);
     }
 
     public override void Suspend ()
@@ -254,14 +298,18 @@ internal class CursesDriver : ConsoleDriver
 
         if (!RunningUnitTests && Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
         {
-            Curses.move (Row, Col);
-
             if (Force16Colors)
             {
+                Curses.move (Row, Col);
+
                 Curses.raw ();
                 Curses.noecho ();
                 Curses.refresh ();
             }
+            else
+            {
+                _mainLoopDriver.WriteRaw (EscSeqUtils.CSI_SetCursorPosition (Row + 1, Col + 1));
+            }
         }
     }
 
@@ -490,14 +538,13 @@ internal class CursesDriver : ConsoleDriver
 
     internal override void End ()
     {
+        _ansiResponseTokenSource?.Cancel ();
+        _ansiResponseTokenSource?.Dispose ();
+        _waitAnsiResponse?.Dispose ();
+
         StopReportingMouseMoves ();
         SetCursorVisibility (CursorVisibility.Default);
 
-        if (_mainLoopDriver is { })
-        {
-            _mainLoopDriver.RemoveWatch (_processInputToken);
-        }
-
         if (RunningUnitTests)
         {
             return;
@@ -567,17 +614,6 @@ internal class CursesDriver : ConsoleDriver
             {
                 Curses.timeout (0);
             }
-
-            _processInputToken = _mainLoopDriver?.AddWatch (
-                                                            0,
-                                                            UnixMainLoop.Condition.PollIn,
-                                                            x =>
-                                                            {
-                                                                ProcessInput ();
-
-                                                                return true;
-                                                            }
-                                                           );
         }
 
         CurrentAttribute = new Attribute (ColorName16.White, ColorName16.Black);
@@ -611,6 +647,7 @@ internal class CursesDriver : ConsoleDriver
         if (!RunningUnitTests)
         {
             Curses.CheckWinChange ();
+            ClearContents ();
 
             if (Force16Colors)
             {
@@ -621,316 +658,48 @@ internal class CursesDriver : ConsoleDriver
         return new MainLoop (_mainLoopDriver);
     }
 
-    internal void ProcessInput ()
+    internal void ProcessInput (UnixMainLoop.PollData inputEvent)
     {
-        int wch;
-        int code = Curses.get_wch (out wch);
-
-        //System.Diagnostics.Debug.WriteLine ($"code: {code}; wch: {wch}");
-        if (code == Curses.ERR)
+        switch (inputEvent.EventType)
         {
-            return;
-        }
+            case UnixMainLoop.EventType.Key:
+                ConsoleKeyInfo consoleKeyInfo = inputEvent.KeyEvent;
 
-        var k = KeyCode.Null;
+                KeyCode map = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
 
-        if (code == Curses.KEY_CODE_YES)
-        {
-            while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize)
-            {
-                ProcessWinChange ();
-                code = Curses.get_wch (out wch);
-            }
-
-            if (wch == 0)
-            {
-                return;
-            }
-
-            if (wch == Curses.KeyMouse)
-            {
-                int wch2 = wch;
-
-                while (wch2 == Curses.KeyMouse)
+                if (map == KeyCode.Null)
                 {
-                    Key kea = null;
-
-                    ConsoleKeyInfo [] cki =
-                    {
-                        new ((char)KeyCode.Esc, 0, false, false, false),
-                        new ('[', 0, false, false, false),
-                        new ('<', 0, false, false, false)
-                    };
-                    code = 0;
-                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki);
-                }
-
-                return;
-            }
-
-            k = MapCursesKey (wch);
-
-            if (wch >= 277 && wch <= 288)
-            {
-                // Shift+(F1 - F12)
-                wch -= 12;
-                k = KeyCode.ShiftMask | MapCursesKey (wch);
-            }
-            else if (wch >= 289 && wch <= 300)
-            {
-                // Ctrl+(F1 - F12)
-                wch -= 24;
-                k = KeyCode.CtrlMask | MapCursesKey (wch);
-            }
-            else if (wch >= 301 && wch <= 312)
-            {
-                // Ctrl+Shift+(F1 - F12)
-                wch -= 36;
-                k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch);
-            }
-            else if (wch >= 313 && wch <= 324)
-            {
-                // Alt+(F1 - F12)
-                wch -= 48;
-                k = KeyCode.AltMask | MapCursesKey (wch);
-            }
-            else if (wch >= 325 && wch <= 327)
-            {
-                // Shift+Alt+(F1 - F3)
-                wch -= 60;
-                k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch);
-            }
-
-            OnKeyDown (new Key (k));
-            OnKeyUp (new Key (k));
-
-            return;
-        }
-
-        // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
-        if (wch == 27)
-        {
-            Curses.timeout (10);
-
-            code = Curses.get_wch (out int wch2);
-
-            if (code == Curses.KEY_CODE_YES)
-            {
-                k = KeyCode.AltMask | MapCursesKey (wch);
-            }
-
-            Key key = null;
-
-            if (code == 0)
-            {
-                // The ESC-number handling, debatable.
-                // Simulates the AltMask itself by pressing Alt + Space.
-                // Needed for macOS
-                if (wch2 == (int)KeyCode.Space)
-                {
-                    k = KeyCode.AltMask | KeyCode.Space;
-                }
-                else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A
-                         && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z)
-                {
-                    k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space));
-                }
-                else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64)
-                {
-                    k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64));
-                }
-                else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9)
-                {
-                    k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0));
-                }
-                else
-                {
-                    ConsoleKeyInfo [] cki =
-                    [
-                        new ((char)KeyCode.Esc, 0, false, false, false), new ((char)wch2, 0, false, false, false)
-                    ];
-                    HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
-
-                    return;
+                    break;
                 }
-                //else if (wch2 == Curses.KeyCSI)
-                //{
-                //    ConsoleKeyInfo [] cki =
-                //    {
-                //        new ((char)KeyCode.Esc, 0, false, false, false), new ('[', 0, false, false, false)
-                //    };
-                //    HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki);
-
-                //    return;
-                //}
-                //else
-                //{
-                //    // Unfortunately there are no way to differentiate Ctrl+Alt+alfa and Ctrl+Shift+Alt+alfa.
-                //    if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0)
-                //    {
-                //        k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~(int)KeyCode.CtrlMask));
-                //    }
-
-                //    if (wch2 == 0)
-                //    {
-                //        k = KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Space;
-                //    }
-                //    //else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
-                //    //{
-                //    //    k = KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Space;
-                //    //}
-                //    else if (wch2 < 256)
-                //    {
-                //        k = (KeyCode)wch2; // | KeyCode.AltMask;
-                //    }
-                //    else
-                //    {
-                //        k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2);
-                //    }
-                //}
-
-                key = new Key (k);
-            }
-            else
-            {
-                key = Key.Esc;
-            }
 
-            OnKeyDown (key);
-            OnKeyUp (key);
-        }
-        else if (wch == Curses.KeyTab)
-        {
-            k = MapCursesKey (wch);
-            OnKeyDown (new Key (k));
-            OnKeyUp (new Key (k));
-        }
-        else if (wch == 127)
-        {
-            // Backspace needed for macOS
-            k = KeyCode.Backspace;
-            OnKeyDown (new Key (k));
-            OnKeyUp (new Key (k));
-        }
-        else
-        {
-            // Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa.
-            k = (KeyCode)wch;
+                OnKeyDown (new Key (map));
+                OnKeyUp (new Key (map));
 
-            if (wch == 0)
-            {
-                k = KeyCode.CtrlMask | KeyCode.Space;
-            }
-            else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64)
-            {
-                if ((KeyCode)(wch + 64) != KeyCode.J)
-                {
-                    k = KeyCode.CtrlMask | (KeyCode)(wch + 64);
-                }
-            }
-            else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z)
-            {
-                k = (KeyCode)wch | KeyCode.ShiftMask;
-            }
-
-            if (wch == '\n' || wch == '\r')
-            {
-                k = KeyCode.Enter;
-            }
+                break;
+            case UnixMainLoop.EventType.Mouse:
+                MouseEventArgs me = new MouseEventArgs { Position = inputEvent.MouseEvent.Position, Flags = inputEvent.MouseEvent.MouseFlags };
+                OnMouseEvent (me);
 
-            // Strip the KeyCode.Space flag off if it's set
-            //if (k != KeyCode.Space && k.HasFlag (KeyCode.Space))
-            if (Key.GetIsKeyCodeAtoZ (k) && (k & KeyCode.Space) != 0)
-            {
-                k &= ~KeyCode.Space;
-            }
+                break;
+            case UnixMainLoop.EventType.WindowSize:
+                Size size = new (inputEvent.WindowSizeEvent.Size.Width, inputEvent.WindowSizeEvent.Size.Height);
+                ProcessWinChange (inputEvent.WindowSizeEvent.Size);
 
-            OnKeyDown (new Key (k));
-            OnKeyUp (new Key (k));
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
         }
     }
 
-    internal void ProcessWinChange ()
+    private void ProcessWinChange (Size size)
     {
-        if (!RunningUnitTests && Curses.CheckWinChange ())
+        if (!RunningUnitTests && Curses.ChangeWindowSize (size.Height, size.Width))
         {
             ClearContents ();
             OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows)));
         }
     }
 
-    private void HandleEscSeqResponse (
-        ref int code,
-        ref KeyCode k,
-        ref int wch2,
-        ref Key keyEventArgs,
-        ref ConsoleKeyInfo [] cki
-    )
-    {
-        ConsoleKey ck = 0;
-        ConsoleModifiers mod = 0;
-
-        while (code == 0)
-        {
-            code = Curses.get_wch (out wch2);
-            var consoleKeyInfo = new ConsoleKeyInfo ((char)wch2, 0, false, false, false);
-
-            if (wch2 == 0 || wch2 == 27 || wch2 == Curses.KeyMouse)
-            {
-                EscSeqUtils.DecodeEscSeq (
-                                          null,
-                                          ref consoleKeyInfo,
-                                          ref ck,
-                                          cki,
-                                          ref mod,
-                                          out _,
-                                          out _,
-                                          out _,
-                                          out _,
-                                          out bool isKeyMouse,
-                                          out List<MouseFlags> mouseFlags,
-                                          out Point pos,
-                                          out _,
-                                          ProcessMouseEvent
-                                         );
-
-                if (isKeyMouse)
-                {
-                    foreach (MouseFlags mf in mouseFlags)
-                    {
-                        ProcessMouseEvent (mf, pos);
-                    }
-
-                    cki = null;
-
-                    if (wch2 == 27)
-                    {
-                        cki = EscSeqUtils.ResizeArray (
-                                                       new ConsoleKeyInfo (
-                                                                           (char)KeyCode.Esc,
-                                                                           0,
-                                                                           false,
-                                                                           false,
-                                                                           false
-                                                                          ),
-                                                       cki
-                                                      );
-                    }
-                }
-                else
-                {
-                    k = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
-                    keyEventArgs = new Key (k);
-                    OnKeyDown (keyEventArgs);
-                }
-            }
-            else
-            {
-                cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki);
-            }
-        }
-    }
-
     private static KeyCode MapCursesKey (int cursesKey)
     {
         switch (cursesKey)
@@ -1008,52 +777,6 @@ internal class CursesDriver : ConsoleDriver
         }
     }
 
-    private void ProcessMouseEvent (MouseFlags mouseFlag, Point pos)
-    {
-        bool WasButtonReleased (MouseFlags flag)
-        {
-            return flag.HasFlag (MouseFlags.Button1Released)
-                   || flag.HasFlag (MouseFlags.Button2Released)
-                   || flag.HasFlag (MouseFlags.Button3Released)
-                   || flag.HasFlag (MouseFlags.Button4Released);
-        }
-
-        bool IsButtonNotPressed (MouseFlags flag)
-        {
-            return !flag.HasFlag (MouseFlags.Button1Pressed)
-                   && !flag.HasFlag (MouseFlags.Button2Pressed)
-                   && !flag.HasFlag (MouseFlags.Button3Pressed)
-                   && !flag.HasFlag (MouseFlags.Button4Pressed);
-        }
-
-        bool IsButtonClickedOrDoubleClicked (MouseFlags flag)
-        {
-            return flag.HasFlag (MouseFlags.Button1Clicked)
-                   || flag.HasFlag (MouseFlags.Button2Clicked)
-                   || flag.HasFlag (MouseFlags.Button3Clicked)
-                   || flag.HasFlag (MouseFlags.Button4Clicked)
-                   || flag.HasFlag (MouseFlags.Button1DoubleClicked)
-                   || flag.HasFlag (MouseFlags.Button2DoubleClicked)
-                   || flag.HasFlag (MouseFlags.Button3DoubleClicked)
-                   || flag.HasFlag (MouseFlags.Button4DoubleClicked);
-        }
-
-        Debug.WriteLine ($"CursesDriver: ({pos.X},{pos.Y}) - {mouseFlag}");
-
-
-        if ((WasButtonReleased (mouseFlag) && IsButtonNotPressed (_lastMouseFlags)) || (IsButtonClickedOrDoubleClicked (mouseFlag) && _lastMouseFlags == 0))
-        {
-            return;
-        }
-
-        _lastMouseFlags = mouseFlag;
-
-        var me = new MouseEventArgs { Flags = mouseFlag, Position = pos };
-        //Debug.WriteLine ($"CursesDriver: ({me.Position}) - {me.Flags}");
-
-        OnMouseEvent (me);
-    }
-
     #region Color Handling
 
     /// <summary>Creates an Attribute from the provided curses-based foreground and background color numbers</summary>

+ 11 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.c

@@ -0,0 +1,11 @@
+#include <stdio.h>
+#include <sys/ioctl.h>
+
+// This function is used to get the value of the TIOCGWINSZ variable,
+// which may have different values ​​on different Unix operating systems.
+// In Linux=0x005413, in Darwin and OpenBSD=0x40087468,
+// In Solaris=0x005468
+// The best solution is having a function that get the real value of the current OS
+int get_tiocgwinsz_value() {
+    return TIOCGWINSZ;
+}

+ 17 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/GetTIOCGWINSZ.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Create output directory if it doesn't exist
+mkdir -p ../../compiled-binaries
+
+# Determine the output file extension based on the OS
+if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+    OUTPUT_FILE="../../compiled-binaries/libGetTIOCGWINSZ.so"
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+    OUTPUT_FILE="../../compiled-binaries/libGetTIOCGWINSZ.dylib"
+else
+    echo "Unsupported OS: $OSTYPE"
+    exit 1
+fi
+
+# Compile the C file
+gcc -shared -fPIC -o "$OUTPUT_FILE" GetTIOCGWINSZ.c

+ 342 - 128
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -2,6 +2,7 @@
 // mainloop.cs: Linux/Curses MainLoop implementation.
 //
 
+using System.Collections.Concurrent;
 using System.Runtime.InteropServices;
 
 namespace Terminal.Gui;
@@ -36,18 +37,13 @@ internal class UnixMainLoop : IMainLoopDriver
         PollNval = 32
     }
 
-    public const int KEY_RESIZE = unchecked ((int)0xffffffffffffffff);
-    private static readonly nint _ignore = Marshal.AllocHGlobal (1);
-
     private readonly CursesDriver _cursesDriver;
-    private readonly Dictionary<int, Watch> _descriptorWatchers = new ();
-    private readonly int [] _wakeUpPipes = new int [2];
     private MainLoop _mainLoop;
-    private bool _pollDirty = true;
     private Pollfd [] _pollMap;
-    private bool _winChanged;
+    private readonly ConcurrentQueue<PollData> _pollDataQueue = new ();
     private readonly ManualResetEventSlim _eventReady = new (false);
     internal readonly ManualResetEventSlim _waitForInput = new (false);
+    private readonly ManualResetEventSlim _windowSizeChange = new (false);
     private readonly CancellationTokenSource _eventReadyTokenSource = new ();
     private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
 
@@ -57,11 +53,13 @@ internal class UnixMainLoop : IMainLoopDriver
         _cursesDriver = (CursesDriver)Application.Driver;
     }
 
+    public EscSeqRequests EscSeqRequests { get; } = new ();
+
     void IMainLoopDriver.Wakeup ()
     {
         if (!ConsoleDriver.RunningUnitTests)
         {
-            write (_wakeUpPipes [1], _ignore, 1);
+            _eventReady.Set ();
         }
     }
 
@@ -76,88 +74,293 @@ internal class UnixMainLoop : IMainLoopDriver
 
         try
         {
-            pipe (_wakeUpPipes);
-
-            AddWatch (
-                      _wakeUpPipes [0],
-                      Condition.PollIn,
-                      ml =>
-                      {
-                          read (_wakeUpPipes [0], _ignore, 1);
-
-                          return true;
-                      }
-                     );
+            // Setup poll for stdin (fd 0) and pipe (fd 1)
+            _pollMap = new Pollfd [1];
+            _pollMap [0].fd = 0;         // stdin (file descriptor 0)
+            _pollMap [0].events = (short)Condition.PollIn; // Monitor input for reading
         }
         catch (DllNotFoundException e)
         {
             throw new NotSupportedException ("libc not found", e);
         }
 
+        EscSeqUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed;
+
         Task.Run (CursesInputHandler, _inputHandlerTokenSource.Token);
+        Task.Run (WindowSizeHandler, _inputHandlerTokenSource.Token);
     }
 
-    internal bool _forceRead;
-    internal bool _suspendRead;
-    private int n;
+    private static readonly int TIOCGWINSZ = GetTIOCGWINSZValue ();
 
-    private void CursesInputHandler ()
+    private const string PlaceholderLibrary = "compiled-binaries/libGetTIOCGWINSZ"; // Placeholder, won't directly load
+
+    [DllImport (PlaceholderLibrary, EntryPoint = "get_tiocgwinsz_value")]
+    private static extern int GetTIOCGWINSZValueInternal ();
+
+    public static int GetTIOCGWINSZValue ()
     {
-        while (_mainLoop is { })
+        // Determine the correct library path based on the OS
+        string libraryPath = Path.Combine (
+                                           AppContext.BaseDirectory,
+                                           "compiled-binaries",
+                                           RuntimeInformation.IsOSPlatform (OSPlatform.OSX) ? "libGetTIOCGWINSZ.dylib" : "libGetTIOCGWINSZ.so");
+
+        // Load the native library manually
+        nint handle = NativeLibrary.Load (libraryPath);
+
+        // Ensure the handle is valid
+        if (handle == nint.Zero)
+        {
+            throw new DllNotFoundException ($"Unable to load library: {libraryPath}");
+        }
+
+        return GetTIOCGWINSZValueInternal ();
+    }
+
+    private void EscSeqUtils_ContinuousButtonPressed (object sender, MouseEventArgs e)
+    {
+        _pollDataQueue!.Enqueue (EnqueueMouseEvent (e.Flags, e.Position));
+    }
+
+    private void WindowSizeHandler ()
+    {
+        var ws = new Winsize ();
+        ioctl (0, TIOCGWINSZ, ref ws);
+
+        // Store initial window size
+        int rows = ws.ws_row;
+        int cols = ws.ws_col;
+
+        while (_inputHandlerTokenSource is { IsCancellationRequested: false })
         {
             try
             {
-                UpdatePollMap ();
+                _windowSizeChange.Wait (_inputHandlerTokenSource.Token);
+                _windowSizeChange.Reset ();
 
-                if (!_inputHandlerTokenSource.IsCancellationRequested && !_forceRead)
+                while (!_inputHandlerTokenSource.IsCancellationRequested)
                 {
-                    _waitForInput.Wait (_inputHandlerTokenSource.Token);
+                    // Wait for a while then check if screen has changed sizes
+                    Task.Delay (500, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
+
+                    ioctl (0, TIOCGWINSZ, ref ws);
+
+                    if (rows != ws.ws_row || cols != ws.ws_col)
+                    {
+                        rows = ws.ws_row;
+                        cols = ws.ws_col;
+
+                        _pollDataQueue!.Enqueue (EnqueueWindowSizeEvent (rows, cols));
+
+                        break;
+                    }
                 }
             }
             catch (OperationCanceledException)
             {
                 return;
             }
-            finally
+
+            _eventReady.Set ();
+        }
+    }
+
+    internal bool _forceRead;
+    private int _retries;
+
+    private void CursesInputHandler ()
+    {
+        while (_mainLoop is { })
+        {
+            try
             {
-                if (!_inputHandlerTokenSource.IsCancellationRequested)
+                if (!_inputHandlerTokenSource.IsCancellationRequested && !_forceRead)
                 {
-                    _waitForInput.Reset ();
+                    _waitForInput.Wait (_inputHandlerTokenSource.Token);
                 }
             }
+            catch (OperationCanceledException)
+            {
+                return;
+            }
 
-            while (!_inputHandlerTokenSource.IsCancellationRequested)
+            if (_pollDataQueue?.Count == 0 || _forceRead)
             {
-                if (!_suspendRead)
+                while (!_inputHandlerTokenSource.IsCancellationRequested)
                 {
-                    n = poll (_pollMap, (uint)_pollMap.Length, 0);
+                    int n = poll (_pollMap, (uint)_pollMap.Length, 0);
 
-                    if (n == KEY_RESIZE)
+                    if (n > 0)
                     {
-                        _winChanged = true;
+                        // Check if stdin has data
+                        if ((_pollMap [0].revents & (int)Condition.PollIn) != 0)
+                        {
+                            // Allocate memory for the buffer
+                            var buf = new byte [2048];
+                            nint bufPtr = Marshal.AllocHGlobal (buf.Length);
+
+                            try
+                            {
+                                // Read from the stdin
+                                int bytesRead = read (_pollMap [0].fd, bufPtr, buf.Length);
+
+                                if (bytesRead > 0)
+                                {
+                                    // Copy the data from unmanaged memory to a byte array
+                                    var buffer = new byte [bytesRead];
+                                    Marshal.Copy (bufPtr, buffer, 0, bytesRead);
+
+                                    // Convert the byte array to a string (assuming UTF-8 encoding)
+                                    string data = Encoding.UTF8.GetString (buffer);
+
+                                    if (EscSeqUtils.IncompleteCkInfos is { })
+                                    {
+                                        data = data.Insert (0, EscSeqUtils.ToString (EscSeqUtils.IncompleteCkInfos));
+                                        EscSeqUtils.IncompleteCkInfos = null;
+                                    }
+
+                                    // Enqueue the data
+                                    ProcessEnqueuePollData (data);
+                                }
+                            }
+                            finally
+                            {
+                                // Free the allocated memory
+                                Marshal.FreeHGlobal (bufPtr);
+                            }
+                        }
+
+                        if (_retries > 0)
+                        {
+                            _retries = 0;
+                        }
 
                         break;
                     }
 
-                    if (n > 0)
+                    if (EscSeqUtils.IncompleteCkInfos is null && EscSeqRequests is { Statuses.Count: > 0 })
                     {
-                        break;
+                        if (_retries > 1)
+                        {
+                            EscSeqRequests.Statuses.TryDequeue (out EscSeqReqStatus seqReqStatus);
+
+                            lock (seqReqStatus.AnsiRequest._responseLock)
+                            {
+                                seqReqStatus.AnsiRequest.Response = string.Empty;
+                                seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, string.Empty);
+                            }
+
+                            _retries = 0;
+                        }
+                        else
+                        {
+                            _retries++;
+                        }
+                    }
+                    else
+                    {
+                        _retries = 0;
                     }
-                }
 
-                if (!_forceRead)
-                {
-                    Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
+                    try
+                    {
+                        Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
+                    }
+                    catch (OperationCanceledException)
+                    {
+                        return;
+                    }
                 }
             }
 
+            _waitForInput.Reset ();
             _eventReady.Set ();
         }
     }
 
+    private void ProcessEnqueuePollData (string pollData)
+    {
+        foreach (string split in EscSeqUtils.SplitEscapeRawString (pollData))
+        {
+            EnqueuePollData (split);
+        }
+    }
+
+    private void EnqueuePollData (string pollDataPart)
+    {
+        ConsoleKeyInfo [] cki = EscSeqUtils.ToConsoleKeyInfoArray (pollDataPart);
+
+        ConsoleKey key = 0;
+        ConsoleModifiers mod = 0;
+        ConsoleKeyInfo newConsoleKeyInfo = default;
+
+        EscSeqUtils.DecodeEscSeq (
+                                  EscSeqRequests,
+                                  ref newConsoleKeyInfo,
+                                  ref key,
+                                  cki,
+                                  ref mod,
+                                  out string c1Control,
+                                  out string code,
+                                  out string [] values,
+                                  out string terminating,
+                                  out bool isMouse,
+                                  out List<MouseFlags> mouseFlags,
+                                  out Point pos,
+                                  out EscSeqReqStatus seqReqStatus,
+                                  EscSeqUtils.ProcessMouseEvent
+                                 );
+
+        if (isMouse)
+        {
+            foreach (MouseFlags mf in mouseFlags)
+            {
+                _pollDataQueue!.Enqueue (EnqueueMouseEvent (mf, pos));
+            }
+
+            return;
+        }
+
+        if (seqReqStatus is { })
+        {
+            var ckiString = EscSeqUtils.ToString (cki);
+
+            lock (seqReqStatus.AnsiRequest._responseLock)
+            {
+                seqReqStatus.AnsiRequest.Response = ckiString;
+                seqReqStatus.AnsiRequest.RaiseResponseFromInput (seqReqStatus.AnsiRequest, ckiString);
+            }
+
+            return;
+        }
+
+        if (newConsoleKeyInfo != default)
+        {
+            _pollDataQueue!.Enqueue (EnqueueKeyboardEvent (newConsoleKeyInfo));
+        }
+    }
+
+    private PollData EnqueueMouseEvent (MouseFlags mouseFlags, Point pos)
+    {
+        var mouseEvent = new MouseEvent { Position = pos, MouseFlags = mouseFlags };
+
+        return new () { EventType = EventType.Mouse, MouseEvent = mouseEvent };
+    }
+
+    private PollData EnqueueKeyboardEvent (ConsoleKeyInfo keyInfo)
+    {
+        return new () { EventType = EventType.Key, KeyEvent = keyInfo };
+    }
+
+    private PollData EnqueueWindowSizeEvent (int rows, int cols)
+    {
+        return new () { EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (cols, rows) } };
+    }
+
     bool IMainLoopDriver.EventsPending ()
     {
         _waitForInput.Set ();
+        _windowSizeChange.Set ();
 
         if (_mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout))
         {
@@ -182,7 +385,7 @@ internal class UnixMainLoop : IMainLoopDriver
 
         if (!_eventReadyTokenSource.IsCancellationRequested)
         {
-            return n > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged;
+            return _pollDataQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
         }
 
         return true;
@@ -190,52 +393,28 @@ internal class UnixMainLoop : IMainLoopDriver
 
     void IMainLoopDriver.Iteration ()
     {
-        if (_winChanged)
-        {
-            _winChanged = false;
-            _cursesDriver.ProcessInput ();
-
-            // This is needed on the mac. See https://github.com/gui-cs/Terminal.Gui/pull/2922#discussion_r1365992426
-            _cursesDriver.ProcessWinChange ();
-        }
-
-        n = 0;
-
-        if (_pollMap is null)
+        // Dequeue and process the data
+        while (_pollDataQueue.TryDequeue (out PollData inputRecords))
         {
-            return;
-        }
-
-        foreach (Pollfd p in _pollMap)
-        {
-            Watch watch;
-
-            if (p.revents == 0)
-            {
-                continue;
-            }
-
-            if (!_descriptorWatchers.TryGetValue (p.fd, out watch))
+            if (inputRecords is { })
             {
-                continue;
-            }
-
-            if (!watch.Callback (_mainLoop))
-            {
-                _descriptorWatchers.Remove (p.fd);
+                _cursesDriver.ProcessInput (inputRecords);
             }
         }
     }
 
     void IMainLoopDriver.TearDown ()
     {
-        _descriptorWatchers?.Clear ();
+        EscSeqUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed;
 
         _inputHandlerTokenSource?.Cancel ();
         _inputHandlerTokenSource?.Dispose ();
-
         _waitForInput?.Dispose ();
 
+        _windowSizeChange.Dispose();
+
+        _pollDataQueue?.Clear ();
+
         _eventReadyTokenSource?.Cancel ();
         _eventReadyTokenSource?.Dispose ();
         _eventReady?.Dispose ();
@@ -243,72 +422,36 @@ internal class UnixMainLoop : IMainLoopDriver
         _mainLoop = null;
     }
 
-    /// <summary>Watches a file descriptor for activity.</summary>
-    /// <remarks>
-    ///     When the condition is met, the provided callback is invoked.  If the callback returns false, the watch is
-    ///     automatically removed. The return value is a token that represents this watch, you can use this token to remove the
-    ///     watch by calling RemoveWatch.
-    /// </remarks>
-    internal object AddWatch (int fileDescriptor, Condition condition, Func<MainLoop, bool> callback)
+    internal void WriteRaw (string ansiRequest)
     {
-        if (callback is null)
-        {
-            throw new ArgumentNullException (nameof (callback));
-        }
-
-        var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor };
-        _descriptorWatchers [fileDescriptor] = watch;
-        _pollDirty = true;
+        // Write to stdout (fd 1)
+        write (STDOUT_FILENO, ansiRequest, ansiRequest.Length);
 
-        return watch;
-    }
-
-    /// <summary>Removes an active watch from the mainloop.</summary>
-    /// <remarks>The token parameter is the value returned from AddWatch</remarks>
-    internal void RemoveWatch (object token)
-    {
-        if (!ConsoleDriver.RunningUnitTests)
-        {
-            if (token is not Watch watch)
-            {
-                return;
-            }
-
-            _descriptorWatchers.Remove (watch.File);
-        }
+        // Flush the stdout buffer immediately using fsync
+        fsync (STDOUT_FILENO);
     }
 
-    [DllImport ("libc")]
-    private static extern int pipe ([In] [Out] int [] pipes);
-
     [DllImport ("libc")]
     private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout);
 
     [DllImport ("libc")]
     private static extern int read (int fd, nint buf, nint n);
 
-    private void UpdatePollMap ()
-    {
-        if (!_pollDirty)
-        {
-            return;
-        }
+    // File descriptor for stdout
+    private const int STDOUT_FILENO = 1;
 
-        _pollDirty = false;
+    [DllImport ("libc")]
+    private static extern int write (int fd, string buf, int n);
 
-        _pollMap = new Pollfd [_descriptorWatchers.Count];
-        var i = 0;
+    [DllImport ("libc", SetLastError = true)]
+    private static extern int fsync (int fd);
 
-        foreach (int fd in _descriptorWatchers.Keys)
-        {
-            _pollMap [i].fd = fd;
-            _pollMap [i].events = (short)_descriptorWatchers [fd].Condition;
-            i++;
-        }
-    }
+    // Get the stdout pointer for flushing
+    [DllImport ("libc", SetLastError = true)]
+    private static extern nint stdout ();
 
-    [DllImport ("libc")]
-    private static extern int write (int fd, nint buf, nint n);
+    [DllImport ("libc", SetLastError = true)]
+    private static extern int ioctl (int fd, int request, ref Winsize ws);
 
     [StructLayout (LayoutKind.Sequential)]
     private struct Pollfd
@@ -324,4 +467,75 @@ internal class UnixMainLoop : IMainLoopDriver
         public Condition Condition;
         public int File;
     }
+
+    /// <summary>
+    ///     Window or terminal size structure. This information is stored by the kernel in order to provide a consistent
+    ///     interface, but is not used by the kernel.
+    /// </summary>
+    [StructLayout (LayoutKind.Sequential)]
+    public struct Winsize
+    {
+        public ushort ws_row;    // Number of rows
+        public ushort ws_col;    // Number of columns
+        public ushort ws_xpixel; // Width in pixels (unused)
+        public ushort ws_ypixel; // Height in pixels (unused)
+    }
+
+    #region Events
+
+    public enum EventType
+    {
+        Key = 1,
+        Mouse = 2,
+        WindowSize = 3
+    }
+
+    public struct MouseEvent
+    {
+        public Point Position;
+        public MouseFlags MouseFlags;
+    }
+
+    public struct WindowSizeEvent
+    {
+        public Size Size;
+    }
+
+    public struct PollData
+    {
+        public EventType EventType;
+        public ConsoleKeyInfo KeyEvent;
+        public MouseEvent MouseEvent;
+        public WindowSizeEvent WindowSizeEvent;
+
+        public readonly override string ToString ()
+        {
+            return EventType switch
+                   {
+                       EventType.Key => ToString (KeyEvent),
+                       EventType.Mouse => MouseEvent.ToString (),
+                       EventType.WindowSize => WindowSizeEvent.ToString (),
+                       _ => "Unknown event type: " + EventType
+                   };
+        }
+
+        /// <summary>Prints a ConsoleKeyInfoEx structure</summary>
+        /// <param name="cki"></param>
+        /// <returns></returns>
+        public readonly string ToString (ConsoleKeyInfo cki)
+        {
+            var ke = new Key ((KeyCode)cki.KeyChar);
+            var sb = new StringBuilder ();
+            sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
+            sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
+            sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
+            string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
+
+            return $"[ConsoleKeyInfo({s})]";
+        }
+    }
+
+    #endregion
 }

+ 13 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs

@@ -143,6 +143,19 @@ public partial class Curses
         return false;
     }
 
+    public static bool ChangeWindowSize (int l, int c)
+    {
+        if (l != lines || c != cols)
+        {
+            lines = l;
+            cols = c;
+
+            return true;
+        }
+
+        return false;
+    }
+
     public static int clearok (nint win, bool bf) { return methods.clearok (win, bf); }
     public static int COLOR_PAIRS () { return methods.COLOR_PAIRS (); }
     public static int curs_set (int visibility) { return methods.curs_set (visibility); }

+ 0 - 2
Terminal.Gui/ConsoleDrivers/CursesDriver/constants.cs

@@ -55,8 +55,6 @@ public partial class Curses
     public const int COLOR_GRAY = 0x8;
     public const int KEY_CODE_YES = 0x100;
     public const int ERR = unchecked ((int)0xffffffff);
-    public const int TIOCGWINSZ = 0x5413;
-    public const int TIOCGWINSZ_MAC = 0x40087468;
     [Flags]
     public enum Event : long
     {

+ 42 - 29
Terminal.Gui/Terminal.Gui.csproj

@@ -11,9 +11,9 @@
   <!-- Assembly name. -->
   <!-- Referenced throughout this file for consistency. -->
   <!-- =================================================================== -->
-<PropertyGroup>
-  <AssemblyName>Terminal.Gui</AssemblyName>
-</PropertyGroup>
+  <PropertyGroup>
+    <AssemblyName>Terminal.Gui</AssemblyName>
+  </PropertyGroup>
 
   <!-- =================================================================== -->
   <!-- .NET Build Settings -->
@@ -143,35 +143,48 @@
   </PropertyGroup>
   <ProjectExtensions><VisualStudio><UserProperties resources_4config_1json__JsonSchema="../../docfx/schemas/tui-config-schema.json" /></VisualStudio></ProjectExtensions>
 
-  <Target Name="CopyNuGetPackagesToLocalPackagesFolder"
-          AfterTargets="Pack"
-          Condition="'$(Configuration)' == 'Release'">
-      <PropertyGroup>
-          <!-- Define the path for local_packages relative to the project directory -->
-          <LocalPackagesPath>$(MSBuildThisFileDirectory)..\local_packages\</LocalPackagesPath>
-          <!-- Output path without framework-specific folders -->
-          <PackageOutputPath>$(MSBuildThisFileDirectory)bin\$(Configuration)\</PackageOutputPath>
-      </PropertyGroup>
+  <Target Name="CopyNuGetPackagesToLocalPackagesFolder" AfterTargets="Pack" Condition="'$(Configuration)' == 'Release'">
+	  <PropertyGroup>
+		  <!-- Define the path for local_packages relative to the project directory -->
+		  <LocalPackagesPath>$(MSBuildThisFileDirectory)..\local_packages\</LocalPackagesPath>
+		  <!-- Output path without framework-specific folders -->
+		  <PackageOutputPath>$(MSBuildThisFileDirectory)bin\$(Configuration)\</PackageOutputPath>
+	  </PropertyGroup>
 
-      <!-- Ensure the local_packages folder exists -->
-      <Message Text="Checking if $(LocalPackagesPath) exists, creating if necessary." Importance="high" />
-      <MakeDir Directories="$(LocalPackagesPath)" />
+	  <!-- Ensure the local_packages folder exists -->
+	  <Message Text="Checking if $(LocalPackagesPath) exists, creating if necessary." Importance="high" />
+	  <MakeDir Directories="$(LocalPackagesPath)" />
 
-      <!-- Collect .nupkg and .snupkg files into an item group -->
-      <ItemGroup>
-          <NuGetPackages Include="$(PackageOutputPath)*.nupkg;$(PackageOutputPath)*.snupkg" />
-      </ItemGroup>
+	  <!-- Collect .nupkg and .snupkg files into an item group -->
+	  <ItemGroup>
+		  <NuGetPackages Include="$(PackageOutputPath)*.nupkg;$(PackageOutputPath)*.snupkg" />
+	  </ItemGroup>
 
-      <!-- Check if any packages were found -->
-      <Message Text="Found packages: @(NuGetPackages)" Importance="high" />
+	  <!-- Check if any packages were found -->
+	  <Message Text="Found packages: @(NuGetPackages)" Importance="high" />
 
-      <!-- Copy files only if found -->
-      <Copy SourceFiles="@(NuGetPackages)"
-            DestinationFolder="$(LocalPackagesPath)"
-            SkipUnchangedFiles="false"
-            Condition="@(NuGetPackages) != ''" />
+	  <!-- Copy files only if found -->
+	  <Copy SourceFiles="@(NuGetPackages)" DestinationFolder="$(LocalPackagesPath)" SkipUnchangedFiles="false"
+	        Condition="@(NuGetPackages) != ''" />
 
-      <!-- Log success -->
-      <Message Text="Copy completed successfully." Importance="high" />
+	  <!-- Log success -->
+	  <Message Text="Copy completed successfully." Importance="high" />
   </Target>
-</Project>
+
+  <ItemGroup>
+	   <!--Include the executable in the NuGet package and set it to copy to output directories-->
+	   <!-- Include the Linux shared library -->
+	  <None Include="$(ProjectDir)compiled-binaries\libGetTIOCGWINSZ.so">
+		  <Pack>true</Pack>
+		  <PackagePath>compiled-binaries/</PackagePath>
+		  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+	  </None>
+
+	   <!-- Include the macOS shared library -->
+	   <None Include="$(ProjectDir)compiled-binaries/libGetTIOCGWINSZ.dylib">
+		   <Pack>true</Pack>
+		   <PackagePath>compiled-binaries/</PackagePath>
+		   <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+	   </None>
+  </ItemGroup>
+</Project>

BIN
Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.dylib


BIN
Terminal.Gui/compiled-binaries/libGetTIOCGWINSZ.so