#nullable enable // // mainloop.cs: Linux/Curses MainLoop implementation. // using System.Runtime.InteropServices; namespace Terminal.Gui; /// Unix main loop, suitable for using on Posix systems /// /// In addition to the general functions of the MainLoop, the Unix version can watch file descriptors using the /// AddWatch methods. /// internal class UnixMainLoop : IMainLoopDriver { /// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions. [Flags] public enum Condition : short { /// There is data to read PollIn = 1, /// Writing to the specified descriptor will not block PollOut = 4, /// There is urgent data to read PollPri = 2, /// Error condition on output PollErr = 8, /// Hang-up on output PollHup = 16, /// File descriptor is not open. 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 _descriptorWatchers = new (); private readonly int [] _wakeUpPipes = new int [2]; private MainLoop? _mainLoop; private bool _pollDirty = true; private Pollfd []? _pollMap; private bool _winChanged; public UnixMainLoop (IConsoleDriver IConsoleDriver) { ArgumentNullException.ThrowIfNull (IConsoleDriver); _cursesDriver = (CursesDriver)IConsoleDriver; } void IMainLoopDriver.Wakeup () { if (!ConsoleDriver.RunningUnitTests) { write (_wakeUpPipes [1], _ignore, 1); } } void IMainLoopDriver.Setup (MainLoop mainLoop) { _mainLoop = mainLoop; if (ConsoleDriver.RunningUnitTests) { return; } try { pipe (_wakeUpPipes); AddWatch ( _wakeUpPipes [0], Condition.PollIn, _ => { read (_wakeUpPipes [0], _ignore, 1); return true; } ); } catch (DllNotFoundException e) { throw new NotSupportedException ("libc not found", e); } } bool IMainLoopDriver.EventsPending () { if (ConsoleDriver.RunningUnitTests) { return true; } UpdatePollMap (); bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout); int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout); if (n == KEY_RESIZE) { _winChanged = true; } return checkTimersResult || n >= KEY_RESIZE; } void IMainLoopDriver.Iteration () { if (ConsoleDriver.RunningUnitTests) { return; } 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 (); } if (_pollMap is null) { return; } foreach (Pollfd p in _pollMap) { if (p.revents == 0) { continue; } if (!_descriptorWatchers.TryGetValue (p.fd, out Watch? watch)) { continue; } if (!watch.Callback (_mainLoop!)) { _descriptorWatchers.Remove (p.fd); } } } void IMainLoopDriver.TearDown () { _descriptorWatchers.Clear (); _mainLoop = null; } /// Watches a file descriptor for activity. /// /// 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. /// internal object AddWatch (int fileDescriptor, Condition condition, Func callback) { ArgumentNullException.ThrowIfNull (callback); var watch = new Watch { Condition = condition, Callback = callback, File = fileDescriptor }; _descriptorWatchers [fileDescriptor] = watch; _pollDirty = true; return watch; } /// Removes an active watch from the mainloop. /// The token parameter is the value returned from AddWatch internal void RemoveWatch (object token) { if (!ConsoleDriver.RunningUnitTests) { if (token is not Watch watch) { return; } _descriptorWatchers.Remove (watch.File); } } private void UpdatePollMap () { if (!_pollDirty) { return; } _pollDirty = false; _pollMap = new Pollfd [_descriptorWatchers.Count]; var i = 0; foreach (int fd in _descriptorWatchers.Keys) { _pollMap [i].fd = fd; _pollMap [i].events = (short)_descriptorWatchers [fd].Condition; i++; } } internal void WriteRaw (string ansiRequest) { // Write to stdout (fd 1) write (STDOUT_FILENO, ansiRequest, ansiRequest.Length); } [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); [DllImport ("libc")] private static extern int write (int fd, nint buf, nint n); // File descriptor for stdout private const int STDOUT_FILENO = 1; [DllImport ("libc")] private static extern int write (int fd, string buf, int n); [StructLayout (LayoutKind.Sequential)] private struct Pollfd { public int fd; public short events; public readonly short revents; } private class Watch { // BUGBUG: Fix this nullable issue. public Func Callback; public Condition Condition; public int File; } }