#nullable enable using System.Runtime.InteropServices; // ReSharper disable IdentifierTypo // ReSharper disable InconsistentNaming // ReSharper disable StringLiteralTypo // ReSharper disable CommentTypo namespace Terminal.Gui.Drivers; internal class UnixInput : InputImpl, IUnixInput { private const int STDIN_FILENO = 0; [StructLayout (LayoutKind.Sequential)] private struct Termios { public uint c_iflag; public uint c_oflag; public uint c_cflag; public uint c_lflag; [MarshalAs (UnmanagedType.ByValArray, SizeConst = 32)] public byte [] c_cc; public uint c_ispeed; public uint c_ospeed; } [DllImport ("libc", SetLastError = true)] private static extern int tcgetattr (int fd, out Termios termios); [DllImport ("libc", SetLastError = true)] private static extern int tcsetattr (int fd, int optional_actions, ref Termios termios); // try cfmakeraw (glibc and macOS usually export it) [DllImport ("libc", EntryPoint = "cfmakeraw", SetLastError = false)] private static extern void cfmakeraw_ref (ref Termios termios); [DllImport ("libc", SetLastError = true)] private static extern nint strerror (int err); private const int TCSANOW = 0; private const ulong BRKINT = 0x00000002; private const ulong ICRNL = 0x00000100; private const ulong INPCK = 0x00000010; private const ulong ISTRIP = 0x00000020; private const ulong IXON = 0x00000400; private const ulong OPOST = 0x00000001; private const ulong ECHO = 0x00000008; private const ulong ICANON = 0x00000100; private const ulong IEXTEN = 0x00008000; private const ulong ISIG = 0x00000001; private const ulong CS8 = 0x00000030; private Termios _original; private bool _terminalInitialized; [StructLayout (LayoutKind.Sequential)] private struct Pollfd { public int fd; public short events; public readonly short revents; } [Flags] private enum Condition : short { PollIn = 1, PollPri = 2, PollOut = 4, PollErr = 8, PollHup = 16, PollNval = 32 } [DllImport ("libc", SetLastError = true)] private static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout); [DllImport ("libc", SetLastError = true)] private static extern int read (int fd, byte [] buf, int count); private const int STDOUT_FILENO = 1; [DllImport ("libc", SetLastError = true)] private static extern int write (int fd, byte [] buf, int count); [DllImport ("libc", SetLastError = true)] private static extern int tcflush (int fd, int queueSelector); private const int TCIFLUSH = 0; private Pollfd []? _pollMap; public UnixInput () { Logging.Information ($"Creating {nameof (UnixInput)}"); try { _pollMap = new Pollfd [1]; _pollMap [0].fd = STDIN_FILENO; _pollMap [0].events = (short)Condition.PollIn; EnableRawModeAndTreatControlCAsInput (); if (_terminalInitialized) { WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); WriteRaw (EscSeqUtils.CSI_HideCursor); WriteRaw (EscSeqUtils.CSI_EnableMouseEvents); } } catch (DllNotFoundException ex) { Logging.Warning ($"UnixInput: libc not available: {ex.Message}. Running in degraded mode."); _terminalInitialized = false; } catch (Exception ex) { Logging.Warning ($"UnixInput: Failed to initialize terminal: {ex.Message}. Running in degraded mode."); _terminalInitialized = false; } } private void EnableRawModeAndTreatControlCAsInput () { try { int result = tcgetattr (STDIN_FILENO, out _original); if (result != 0) { int e = Marshal.GetLastWin32Error (); Logging.Warning ($"tcgetattr failed errno={e} ({StrError (e)}). Running without TTY support."); return; } Termios raw = _original; try { cfmakeraw_ref (ref raw); } catch (EntryPointNotFoundException) { raw.c_iflag &= ~((uint)BRKINT | (uint)ICRNL | (uint)INPCK | (uint)ISTRIP | (uint)IXON); raw.c_oflag &= ~(uint)OPOST; raw.c_cflag |= (uint)CS8; raw.c_lflag &= ~((uint)ECHO | (uint)ICANON | (uint)IEXTEN | (uint)ISIG); } result = tcsetattr (STDIN_FILENO, TCSANOW, ref raw); if (result != 0) { int e = Marshal.GetLastWin32Error (); Logging.Warning ($"tcsetattr failed errno={e} ({StrError (e)}). Running without TTY support."); return; } _terminalInitialized = true; } catch (DllNotFoundException) { throw; // Re-throw to be caught by constructor } } private string StrError (int err) { try { nint p = strerror (err); return p == nint.Zero ? $"errno={err}" : Marshal.PtrToStringAnsi (p) ?? $"errno={err}"; } catch { return $"errno={err}"; } } /// public override bool Peek () { if (!_terminalInitialized || _pollMap is null) { return false; } try { int n = poll (_pollMap, (uint)_pollMap.Length, 0); return n != 0; } catch (Exception ex) { Logging.Error ($"Error in Peek: {ex.Message}"); return false; } } private void WriteRaw (string text) { if (!_terminalInitialized) { return; } try { byte [] utf8 = Encoding.UTF8.GetBytes (text); write (STDOUT_FILENO, utf8, utf8.Length); } catch { // ignore exceptions during write } } /// public override IEnumerable Read () { if (!_terminalInitialized || _pollMap is null) { yield break; } while (poll (_pollMap, (uint)_pollMap.Length, 0) != 0) { if ((_pollMap [0].revents & (int)Condition.PollIn) != 0) { var buf = new byte [256]; int bytesRead = read (0, buf, buf.Length); string input = Encoding.UTF8.GetString (buf, 0, bytesRead); foreach (char ch in input) { yield return ch; } } } } private void FlushConsoleInput () { if (!_terminalInitialized) { return; } try { Pollfd [] fds = new Pollfd [1]; fds [0].fd = STDIN_FILENO; fds [0].events = (short)Condition.PollIn; var buf = new byte [256]; while (poll (fds, 1, 0) > 0) { read (STDIN_FILENO, buf, buf.Length); } } catch { // ignore } } /// public override void Dispose () { base.Dispose (); if (!_terminalInitialized) { return; } try { WriteRaw (EscSeqUtils.CSI_DisableMouseEvents); FlushConsoleInput (); tcflush (STDIN_FILENO, TCIFLUSH); WriteRaw (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); WriteRaw (EscSeqUtils.CSI_ShowCursor); tcsetattr (STDIN_FILENO, TCSANOW, ref _original); } catch { // ignore exceptions during disposal } } }