//
// mainloop.cs: Linux/Curses MainLoop implementation.
//
using System.Collections.Concurrent;
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
}
private readonly CursesDriver _cursesDriver;
private MainLoop _mainLoop;
private Pollfd [] _pollMap;
private readonly ConcurrentQueue _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 ();
public UnixMainLoop (ConsoleDriver consoleDriver = null)
{
_cursesDriver = (CursesDriver)consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
}
public AnsiEscapeSequenceRequests EscSeqRequests { get; } = new ();
void IMainLoopDriver.Wakeup () { _eventReady.Set (); }
void IMainLoopDriver.Setup (MainLoop mainLoop)
{
_mainLoop = mainLoop;
if (ConsoleDriver.RunningUnitTests)
{
return;
}
try
{
// Setup poll for stdin (fd 0)
_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);
}
AnsiEscapeSequenceRequestUtils.ContinuousButtonPressed += EscSeqUtils_ContinuousButtonPressed;
Task.Run (CursesInputHandler, _inputHandlerTokenSource.Token);
Task.Run (WindowSizeHandler, _inputHandlerTokenSource.Token);
}
private static readonly int TIOCGWINSZ = GetTIOCGWINSZValue ();
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 ()
{
// 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
{
_windowSizeChange.Wait (_inputHandlerTokenSource.Token);
_windowSizeChange.Reset ();
while (!_inputHandlerTokenSource.IsCancellationRequested)
{
// 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;
}
_eventReady.Set ();
}
}
internal bool _forceRead;
private int _retries;
private void CursesInputHandler ()
{
while (_mainLoop is { })
{
try
{
if (!_inputHandlerTokenSource.IsCancellationRequested && !_forceRead)
{
_waitForInput.Wait (_inputHandlerTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return;
}
finally
{
if (!_inputHandlerTokenSource.IsCancellationRequested)
{
_waitForInput.Reset ();
}
}
if (_pollDataQueue?.Count == 0 || _forceRead)
{
while (!_inputHandlerTokenSource.IsCancellationRequested)
{
int n = poll (_pollMap, (uint)_pollMap.Length, 0);
if (n > 0)
{
// 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 (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is { })
{
data = data.Insert (0, AnsiEscapeSequenceRequestUtils.ToString (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos));
AnsiEscapeSequenceRequestUtils.IncompleteCkInfos = null;
}
// Enqueue the data
ProcessEnqueuePollData (data);
}
}
finally
{
// Free the allocated memory
Marshal.FreeHGlobal (bufPtr);
}
}
if (_retries > 0)
{
_retries = 0;
}
break;
}
if (AnsiEscapeSequenceRequestUtils.IncompleteCkInfos is null && EscSeqRequests is { Statuses.Count: > 0 })
{
if (_retries > 1)
{
if (EscSeqRequests.Statuses.TryPeek (out AnsiEscapeSequenceRequestStatus seqReqStatus) && seqReqStatus.AnsiRequest.AnsiEscapeSequenceResponse is { } && string.IsNullOrEmpty (seqReqStatus.AnsiRequest.AnsiEscapeSequenceResponse.Response))
{
lock (seqReqStatus!.AnsiRequest._responseLock)
{
EscSeqRequests.Statuses.TryDequeue (out _);
seqReqStatus.AnsiRequest.RaiseResponseFromInput (null);
}
}
_retries = 0;
}
else
{
_retries++;
}
}
else
{
_retries = 0;
}
try
{
if (!_forceRead)
{
Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return;
}
}
}
_eventReady.Set ();
}
}
private void ProcessEnqueuePollData (string pollData)
{
foreach (string split in AnsiEscapeSequenceRequestUtils.SplitEscapeRawString (pollData))
{
EnqueuePollData (split);
}
}
private void EnqueuePollData (string pollDataPart)
{
ConsoleKeyInfo [] cki = AnsiEscapeSequenceRequestUtils.ToConsoleKeyInfoArray (pollDataPart);
ConsoleKey key = 0;
ConsoleModifiers mod = 0;
ConsoleKeyInfo newConsoleKeyInfo = default;
AnsiEscapeSequenceRequestUtils.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,
out Point pos,
out AnsiEscapeSequenceRequestStatus seqReqStatus,
AnsiEscapeSequenceRequestUtils.ProcessMouseEvent
);
if (isMouse)
{
foreach (MouseFlags mf in mouseFlags)
{
_pollDataQueue!.Enqueue (EnqueueMouseEvent (mf, pos));
}
return;
}
if (seqReqStatus is { })
{
var ckiString = AnsiEscapeSequenceRequestUtils.ToString (cki);
lock (seqReqStatus.AnsiRequest._responseLock)
{
seqReqStatus.AnsiRequest.RaiseResponseFromInput (ckiString);
}
return;
}
if (!string.IsNullOrEmpty (AnsiEscapeSequenceRequestUtils.InvalidRequestTerminator))
{
if (EscSeqRequests.Statuses.TryDequeue (out AnsiEscapeSequenceRequestStatus result))
{
lock (result.AnsiRequest._responseLock)
{
result.AnsiRequest.RaiseResponseFromInput (AnsiEscapeSequenceRequestUtils.InvalidRequestTerminator);
AnsiEscapeSequenceRequestUtils.InvalidRequestTerminator = null;
}
}
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))
{
return true;
}
try
{
if (!_eventReadyTokenSource.IsCancellationRequested)
{
_eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token);
}
}
catch (OperationCanceledException)
{
return true;
}
finally
{
_eventReady.Reset ();
}
if (!_eventReadyTokenSource.IsCancellationRequested)
{
return _pollDataQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
}
return true;
}
void IMainLoopDriver.Iteration ()
{
// Dequeue and process the data
while (_pollDataQueue.TryDequeue (out PollData inputRecords))
{
if (inputRecords is { })
{
_cursesDriver.ProcessInput (inputRecords);
}
}
}
void IMainLoopDriver.TearDown ()
{
AnsiEscapeSequenceRequestUtils.ContinuousButtonPressed -= EscSeqUtils_ContinuousButtonPressed;
_inputHandlerTokenSource?.Cancel ();
_inputHandlerTokenSource?.Dispose ();
_waitForInput?.Dispose ();
_windowSizeChange.Dispose();
_pollDataQueue?.Clear ();
_eventReadyTokenSource?.Cancel ();
_eventReadyTokenSource?.Dispose ();
_eventReady?.Dispose ();
_mainLoop = null;
}
internal void WriteRaw (string ansiRequest)
{
// Write to stdout (fd 1)
write (STDOUT_FILENO, ansiRequest, ansiRequest.Length);
Task.Delay (100, _inputHandlerTokenSource.Token).Wait (_inputHandlerTokenSource.Token);
}
[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);
// File descriptor for stdout
private const int STDOUT_FILENO = 1;
[DllImport ("libc")]
private static extern int write (int fd, string buf, int n);
[DllImport ("libc", SetLastError = true)]
private static extern int ioctl (int fd, int request, ref Winsize ws);
[StructLayout (LayoutKind.Sequential)]
private struct Pollfd
{
public int fd;
public short events;
public readonly short revents;
}
///
/// 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.
///
[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
};
}
/// Prints a ConsoleKeyInfoEx structure
///
///
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
}