2
0
Эх сурвалжийг харах

Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop

Tig 2 сар өмнө
parent
commit
d2c8693abe

+ 32 - 2
Examples/UICatalog/Properties/launchSettings.json

@@ -42,6 +42,12 @@
       "commandLineArgs": "dotnet UICatalog.dll --driver v2",
       "distributionName": ""
     },
+    "WSL: UICatalog --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2unix",
+      "distributionName": ""
+    },
     "WSL: UICatalog --driver v2net": {
       "commandName": "Executable",
       "executablePath": "wsl",
@@ -63,13 +69,19 @@
     "WSL-Gnome: UICatalog --driver v2": {
       "commandName": "Executable",
       "executablePath": "wsl",
-      "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2; exec bash\"'",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2; exec bash\"'",
+      "distributionName": ""
+    },
+    "WSL-Gnome: UICatalog --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2unix; exec bash\"'",
       "distributionName": ""
     },
     "WSL-Gnome: UICatalog --driver v2net": {
       "commandName": "Executable",
       "executablePath": "wsl",
-      "commandLineArgs": "bash -c 'while [ ! -e \"$XDG_RUNTIME_DIR/bus\" ]; do sleep 0.1; done; gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2net; exec bash\"'",
+      "commandLineArgs": "bash -c 'dbus-run-session -- gnome-terminal --wait -- bash -l -c \"dotnet UICatalog.dll --driver v2net; exec bash\"'",
       "distributionName": ""
     },
     "Benchmark All": {
@@ -94,6 +106,24 @@
       "commandLineArgs": "dotnet UICatalog.dll --benchmark",
       "distributionName": ""
     },
+    "WSL: Benchmark All --driver v2": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2 --benchmark",
+      "distributionName": ""
+    },
+    "WSL: Benchmark All --driver v2unix": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2unix --benchmark",
+      "distributionName": ""
+    },
+    "WSL: Benchmark All --driver v2net": {
+      "commandName": "Executable",
+      "executablePath": "wsl",
+      "commandLineArgs": "dotnet UICatalog.dll --driver v2net --benchmark",
+      "distributionName": ""
+    },
     "Docker": {
       "commandName": "Docker"
     },

+ 1 - 1
Terminal.Gui/App/Application.Initialization.cs

@@ -217,7 +217,7 @@ public static partial class Application // Initialization (Init/Shutdown)
         List<string?> driverTypeNames = driverTypes
                                         .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d))
                                         .Select (d => d!.Name)
-                                        .Union (["v2", "v2win", "v2net"])
+                                        .Union (["v2", "v2win", "v2net", "v2unix"])
                                         .ToList ()!;
 
         return (driverTypes, driverTypeNames);

+ 9 - 2
Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs

@@ -8,7 +8,7 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern
     public EscAsAltPattern () { IsLastMinute = true; }
 
 #pragma warning disable IDE1006 // Naming Styles
-    private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$");
+    private static readonly Regex _pattern = new (@"^\u001b([\u0001-\u001a\u001fa-zA-Z0-9_])$");
 #pragma warning restore IDE1006 // Naming Styles
 
     public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
@@ -22,7 +22,14 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern
             return null;
         }
 
-        char key = match.Groups [1].Value [0];
+        char ch = match.Groups [1].Value [0];
+
+        Key key = ch switch
+                  {
+                      >= '\u0001' and <= '\u001a' => ((Key)(ch + 96)).WithCtrl,
+                      '\u001f' => Key.D7.WithCtrl.WithShift,
+                      _ => ch
+                  };
 
         return new Key (key).WithAlt;
     }

+ 8 - 1
Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs

@@ -10,7 +10,7 @@ namespace Terminal.Gui.Drivers;
 public class Ss3Pattern : AnsiKeyboardParserPattern
 {
 #pragma warning disable IDE1006 // Naming Styles
-    private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$");
+    private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCABOHFwqysu])$");
 #pragma warning restore IDE1006 // Naming Styles
 
     /// <inheritdoc/>
@@ -41,6 +41,13 @@ public class Ss3Pattern : AnsiKeyboardParserPattern
                    'C' => Key.CursorRight,
                    'A' => Key.CursorUp,
                    'B' => Key.CursorDown,
+                   'H' => Key.Home,
+                   'F' => Key.End,
+                   'w' => Key.Home,
+                   'q' => Key.End,
+                   'y' => Key.PageUp,
+                   's' => Key.PageDown,
+                   'u' => Key.Clear,
                    _ => null
                };
     }

+ 53 - 3
Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs

@@ -1029,6 +1029,16 @@ public static class EscSeqUtils
         //}
     }
 
+    /// <summary>
+    /// Helper to set the Control key states based on the char.
+    /// </summary>
+    /// <param name="ch">The char value.</param>
+    /// <returns></returns>
+    public static ConsoleKeyInfo MapChar (char ch)
+    {
+        return MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false));
+    }
+
     /// <summary>
     ///     Ensures a console key is mapped to one that works correctly with ANSI escape sequences.
     /// </summary>
@@ -1131,6 +1141,17 @@ public static class EscSeqUtils
                                              true);
                 }
 
+                break;
+            case uint n when n is >= '\u001c'  and <= '\u001f':
+                key = (ConsoleKey)(char)(consoleKeyInfo.KeyChar + 24);
+
+                newConsoleKeyInfo = new (
+                                         (char)key,
+                                         key,
+                                         (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0,
+                                         (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0,
+                                         true);
+
                 break;
             case 127: // DEL
                 key = ConsoleKey.Backspace;
@@ -1375,6 +1396,12 @@ public static class EscSeqUtils
     {
         switch (keyInfo.Key)
         {
+            case ConsoleKey.Multiply:
+            case ConsoleKey.Add:
+            case ConsoleKey.Separator:
+            case ConsoleKey.Subtract:
+            case ConsoleKey.Decimal:
+            case ConsoleKey.Divide:
             case ConsoleKey.OemPeriod:
             case ConsoleKey.OemComma:
             case ConsoleKey.OemPlus:
@@ -1391,8 +1418,31 @@ public static class EscSeqUtils
             case ConsoleKey.Oem102:
                 if (keyInfo.KeyChar == 0)
                 {
-                    // If the keyChar is 0, keyInfo.Key value is not a printable character.
-                    System.Diagnostics.Debug.Assert (keyInfo.Key == 0);
+                    // All Oem* produce a valid KeyChar and is not guaranteed to be printable ASCII, but it’s never just '\0' (null).
+                    // If that happens it's because Console.ReadKey is misreporting for AltGr + non-character keys
+                    // or if it's a combine key waiting for the next input which will determine the respective KeyChar.
+                    // This behavior only happens on Windows and not on Unix-like systems.
+                    if (keyInfo.Key != ConsoleKey.Multiply
+                        && keyInfo.Key != ConsoleKey.Add
+                        && keyInfo.Key != ConsoleKey.Decimal
+                        && keyInfo.Key != ConsoleKey.Subtract
+                        && keyInfo.Key != ConsoleKey.Divide
+                        && keyInfo.Key != ConsoleKey.OemPeriod
+                        && keyInfo.Key != ConsoleKey.OemComma
+                        && keyInfo.Key != ConsoleKey.OemPlus
+                        && keyInfo.Key != ConsoleKey.OemMinus
+                        && keyInfo.Key != ConsoleKey.Oem1
+                        && keyInfo.Key != ConsoleKey.Oem2
+                        && keyInfo.Key != ConsoleKey.Oem3
+                        && keyInfo.Key != ConsoleKey.Oem4
+                        && keyInfo.Key != ConsoleKey.Oem5
+                        && keyInfo.Key != ConsoleKey.Oem6
+                        && keyInfo.Key != ConsoleKey.Oem7
+                        && keyInfo.Key != ConsoleKey.Oem102)
+                    {
+                        // If the keyChar is 0, keyInfo.Key value is not a printable character.
+                        System.Diagnostics.Debug.Assert (keyInfo.Key == 0);
+                    }
 
                     return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key);
                 }
@@ -1411,7 +1461,7 @@ public static class EscSeqUtils
         // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
         if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key))
         {
-            if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I)
+            if (keyInfo is { Modifiers: ConsoleModifiers.Control, Key: ConsoleKey.I })
             {
                 return KeyCode.Tab;
             }

+ 19 - 40
Terminal.Gui/Drivers/V2/ApplicationV2.cs

@@ -81,24 +81,29 @@ public class ApplicationV2 : ApplicationImpl
     {
         PlatformID p = Environment.OSVersion.Platform;
 
-        bool definetlyWin = (driverName?.Contains ("win") ?? false )|| _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
-        bool definetlyNet = (driverName?.Contains ("net") ?? false ) || _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+        bool definetlyWin = (driverName?.Contains ("win") ?? false) || _componentFactory is IComponentFactory<WindowsConsole.InputRecord>;
+        bool definetlyNet = (driverName?.Contains ("net") ?? false) || _componentFactory is IComponentFactory<ConsoleKeyInfo>;
+        bool definetlyUnix = (driverName?.Contains ("unix") ?? false) || _componentFactory is IComponentFactory<char>;
 
         if (definetlyWin)
         {
-            _coordinator = CreateWindowsSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
         }
         else if (definetlyNet)
         {
-            _coordinator = CreateNetSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new NetComponentFactory ());
+        }
+        else if (definetlyUnix)
+        {
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
         }
         else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
         {
-            _coordinator = CreateWindowsSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new WindowsComponentFactory ());
         }
         else
         {
-            _coordinator = CreateNetSubcomponents ();
+            _coordinator = CreateSubcomponents (() => new UnixComponentFactory ());
         }
 
         _coordinator.StartAsync ().Wait ();
@@ -109,49 +114,23 @@ public class ApplicationV2 : ApplicationImpl
         }
     }
 
-    private IMainLoopCoordinator CreateWindowsSubcomponents ()
-    {
-        ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ();
-        MainLoop<WindowsConsole.InputRecord> loop = new ();
-
-        IComponentFactory<WindowsConsole.InputRecord> cf;
-
-        if (_componentFactory != null)
-        {
-            cf = (IComponentFactory<WindowsConsole.InputRecord>)_componentFactory;
-        }
-        else
-        {
-            cf = new WindowsComponentFactory ();
-        }
-
-        return new MainLoopCoordinator<WindowsConsole.InputRecord> (_timedEvents,
-                                                                    inputBuffer,
-                                                                    loop,
-                                                                    cf);
-    }
-
-    private IMainLoopCoordinator CreateNetSubcomponents ()
+    private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
     {
-        ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ();
-        MainLoop<ConsoleKeyInfo> loop = new ();
+        ConcurrentQueue<T> inputBuffer = new ();
+        MainLoop<T> loop = new ();
 
-        IComponentFactory<ConsoleKeyInfo> cf;
+        IComponentFactory<T> cf;
 
-        if (_componentFactory != null)
+        if (_componentFactory is IComponentFactory<T> typedFactory)
         {
-            cf = (IComponentFactory<ConsoleKeyInfo>)_componentFactory;
+            cf = typedFactory;
         }
         else
         {
-            cf = new NetComponentFactory ();
+            cf = fallbackFactory ();
         }
 
-        return new MainLoopCoordinator<ConsoleKeyInfo> (
-                                                        _timedEvents,
-                                                        inputBuffer,
-                                                        loop,
-                                                        cf);
+        return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
     }
 
     /// <inheritdoc/>

+ 1 - 10
Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs

@@ -260,16 +260,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
     /// <inheritdoc/>
     public virtual string GetVersionInfo ()
     {
-        var type = "";
-
-        if (InputProcessor is WindowsInputProcessor)
-        {
-            type = "win";
-        }
-        else if (InputProcessor is NetInputProcessor)
-        {
-            type = "net";
-        }
+        string type = InputProcessor.DriverName ?? throw new ArgumentNullException (nameof (InputProcessor.DriverName));
 
         return "v2" + type;
     }

+ 5 - 0
Terminal.Gui/Drivers/V2/IInputProcessor.cs

@@ -25,6 +25,11 @@ public interface IInputProcessor
     /// <summary>Event fired when a mouse event occurs.</summary>
     event EventHandler<MouseEventArgs>? MouseEvent;
 
+    /// <summary>
+    /// Gets the name of the driver associated with this input processor.
+    /// </summary>
+    string DriverName { get; init; }
+
     /// <summary>
     ///     Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
     ///     <see cref="OnKeyUp"/>.

+ 3 - 0
Terminal.Gui/Drivers/V2/IUnixInput.cs

@@ -0,0 +1,3 @@
+namespace Terminal.Gui.Drivers;
+
+internal interface IUnixInput : IConsoleInput<char>;

+ 3 - 0
Terminal.Gui/Drivers/V2/InputProcessor.cs

@@ -30,6 +30,9 @@ public abstract class InputProcessor<T> : IInputProcessor
     /// </summary>
     public ConcurrentQueue<T> InputBuffer { get; }
 
+    /// <inheritdoc />
+    public string DriverName { get; init; }
+
     /// <inheritdoc/>
     public IAnsiResponseParser GetParser () { return Parser; }
 

+ 16 - 0
Terminal.Gui/Drivers/V2/NetInput.cs

@@ -70,10 +70,23 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
         }
     }
 
+    private void FlushConsoleInput ()
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            while (Console.KeyAvailable)
+            {
+                Console.ReadKey (intercept: true);
+            }
+        }
+    }
+
     /// <inheritdoc/>
     public override void Dispose ()
     {
         base.Dispose ();
+
+        // Disable mouse events first
         Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
 
         //Disable alternative screen buffer.
@@ -83,5 +96,8 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput
         Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
 
         _adjustConsole?.Cleanup ();
+
+        // Flush any pending input so no stray events appear
+        FlushConsoleInput ();
     }
 }

+ 4 - 1
Terminal.Gui/Drivers/V2/NetInputProcessor.cs

@@ -20,7 +20,10 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
 #pragma warning restore CA2211
 
     /// <inheritdoc/>
-    public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ()) { }
+    public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ())
+    {
+        DriverName = "net";
+    }
 
     /// <inheritdoc/>
     protected override void Process (ConsoleKeyInfo consoleKeyInfo)

+ 38 - 15
Terminal.Gui/Drivers/V2/NetOutput.cs

@@ -54,24 +54,31 @@ public class NetOutput : OutputBase, IConsoleOutput
     /// <inheritdoc/>
     protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
     {
-        EscSeqUtils.CSI_AppendForegroundColorRGB (
-                                                  output,
-                                                  attr.Foreground.R,
-                                                  attr.Foreground.G,
-                                                  attr.Foreground.B
-                                                 );
-
-        EscSeqUtils.CSI_AppendBackgroundColorRGB (
-                                                  output,
-                                                  attr.Background.R,
-                                                  attr.Background.G,
-                                                  attr.Background.B
-                                                 );
+        if (Application.Force16Colors)
+        {
+            output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
+            output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+        }
+        else
+        {
+            EscSeqUtils.CSI_AppendForegroundColorRGB (
+                                                      output,
+                                                      attr.Foreground.R,
+                                                      attr.Foreground.G,
+                                                      attr.Foreground.B
+                                                     );
+
+            EscSeqUtils.CSI_AppendBackgroundColorRGB (
+                                                      output,
+                                                      attr.Background.R,
+                                                      attr.Background.G,
+                                                      attr.Background.B
+                                                     );
+        }
 
         EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
     }
 
-
     /// <inheritdoc />
     protected override void Write (StringBuilder output)
     {
@@ -116,9 +123,25 @@ public class NetOutput : OutputBase, IConsoleOutput
     }
 
 
+    private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
+
     /// <inheritdoc cref="IConsoleOutput.SetCursorVisibility"/>
     public override void SetCursorVisibility (CursorVisibility visibility)
     {
-        Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+        if (visibility != CursorVisibility.Invisible)
+        {
+            if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
+            {
+                _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
+
+                Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
+            }
+
+            Write (EscSeqUtils.CSI_ShowCursor);
+        }
+        else
+        {
+            Write (EscSeqUtils.CSI_HideCursor);
+        }
     }
 }

+ 29 - 0
Terminal.Gui/Drivers/V2/UnixComponentFactory.cs

@@ -0,0 +1,29 @@
+#nullable enable
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+/// <see cref="IComponentFactory{T}"/> implementation for native unix console I/O i.e. v2unix.
+/// This factory creates instances of internal classes <see cref="UnixInput"/>, <see cref="UnixOutput"/> etc.
+/// </summary>
+public class UnixComponentFactory : ComponentFactory<char>
+{
+    /// <inheritdoc />
+    public override IConsoleInput<char> CreateInput ()
+    {
+        return new UnixInput ();
+    }
+
+    /// <inheritdoc />
+    public override IInputProcessor CreateInputProcessor (ConcurrentQueue<char> inputBuffer)
+    {
+        return new UnixInputProcessor (inputBuffer);
+    }
+
+    /// <inheritdoc />
+    public override IConsoleOutput CreateOutput ()
+    {
+        return new UnixOutput ();
+    }
+}

+ 266 - 0
Terminal.Gui/Drivers/V2/UnixInput.cs

@@ -0,0 +1,266 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+
+namespace Terminal.Gui.Drivers;
+
+internal class UnixInput : ConsoleInput<char>, 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;
+
+    [StructLayout (LayoutKind.Sequential)]
+    private struct Pollfd
+    {
+        public int fd;
+        public short events;
+        public readonly short revents; // readonly signals "don't touch this in managed code"
+    }
+
+    /// <summary>Condition on which to wake up from file descriptor activity.  These match the Linux/BSD poll definitions.</summary>
+    [Flags]
+    private enum Condition : short
+    {
+        /// <summary>There is data to read</summary>
+        PollIn = 1,
+
+        /// <summary>There is urgent data to read</summary>
+        PollPri = 2,
+
+        /// <summary>Writing to the specified descriptor will not block</summary>
+        PollOut = 4,
+
+        /// <summary>Error condition on output</summary>
+        PollErr = 8,
+
+        /// <summary>Hang-up on output</summary>
+        PollHup = 16,
+
+        /// <summary>File descriptor is not open.</summary>
+        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);
+
+    // File descriptor for stdout
+    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;  // flush data received but not read
+
+    private Pollfd [] _pollMap;
+
+    public UnixInput ()
+    {
+        Logging.Logger.LogInformation ($"Creating {nameof (UnixInput)}");
+
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            return;
+        }
+
+        _pollMap = new Pollfd [1];
+        _pollMap [0].fd = STDIN_FILENO; // stdin
+        _pollMap [0].events = (short)Condition.PollIn;
+
+        EnableRawModeAndTreatControlCAsInput ();
+
+        //Enable alternative screen buffer.
+        WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+        //Set cursor key to application.
+        WriteRaw (EscSeqUtils.CSI_HideCursor);
+
+        WriteRaw (EscSeqUtils.CSI_EnableMouseEvents);
+    }
+
+    private void EnableRawModeAndTreatControlCAsInput ()
+    {
+        if (tcgetattr (STDIN_FILENO, out _original) != 0)
+        {
+            var e = Marshal.GetLastWin32Error ();
+            throw new InvalidOperationException ($"tcgetattr failed errno={e} ({StrError (e)})");
+        }
+
+        var raw = _original;
+
+        // Prefer cfmakeraw if available
+        try
+        {
+            cfmakeraw_ref (ref raw);
+        }
+        catch (EntryPointNotFoundException)
+        {
+            // fallback: roughly cfmakeraw equivalent
+            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);
+        }
+
+        if (tcsetattr (STDIN_FILENO, TCSANOW, ref raw) != 0)
+        {
+            var e = Marshal.GetLastWin32Error ();
+            throw new InvalidOperationException ($"tcsetattr failed errno={e} ({StrError (e)})");
+        }
+    }
+
+    private string StrError (int err)
+    {
+        var p = strerror (err);
+        return p == nint.Zero ? $"errno={err}" : Marshal.PtrToStringAnsi (p) ?? $"errno={err}";
+    }
+
+    /// <inheritdoc />
+    protected override bool Peek ()
+    {
+        try
+        {
+            if (ConsoleDriver.RunningUnitTests)
+            {
+                return false;
+            }
+
+            int n = poll (_pollMap!, (uint)_pollMap!.Length, 0);
+
+            if (n != 0)
+            {
+                return true;
+            }
+
+            return false;
+        }
+        catch (Exception ex)
+        {
+            // Optionally log the exception
+            Logging.Logger.LogError ($"Error in Peek: {ex.Message}");
+
+            return false;
+        }
+    }
+    private void WriteRaw (string text)
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            byte [] utf8 = Encoding.UTF8.GetBytes (text);
+            // Write to stdout (fd 1)
+            write (STDOUT_FILENO, utf8, utf8.Length);
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override IEnumerable<char> Read ()
+    {
+        while (poll (_pollMap!, (uint)_pollMap!.Length, 0) != 0)
+        {
+            // Check if stdin has data
+            if ((_pollMap [0].revents & (int)Condition.PollIn) != 0)
+            {
+                var buf = new byte [256];
+                int bytesRead = read (0, buf, buf.Length); // Read from stdin
+                string input = Encoding.UTF8.GetString (buf, 0, bytesRead);
+
+                foreach (char ch in input)
+                {
+                    yield return ch;
+                }
+            }
+        }
+    }
+
+    private void FlushConsoleInput ()
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            var 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);
+            }
+        }
+    }
+
+    /// <inheritdoc />
+    public override void Dispose ()
+    {
+        base.Dispose ();
+
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            // Disable mouse events first
+            WriteRaw (EscSeqUtils.CSI_DisableMouseEvents);
+
+            // Drain any pending input already queued by the terminal
+            FlushConsoleInput ();
+
+            // Flush kernel input buffer
+            tcflush (STDIN_FILENO, TCIFLUSH);
+
+            //Disable alternative screen buffer.
+            WriteRaw (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            WriteRaw (EscSeqUtils.CSI_ShowCursor);
+
+            // Restore terminal to original state
+            tcsetattr (STDIN_FILENO, TCSANOW, ref _original);
+        }
+    }
+}

+ 38 - 0
Terminal.Gui/Drivers/V2/UnixInputProcessor.cs

@@ -0,0 +1,38 @@
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Input processor for <see cref="UnixInput"/>, deals in <see cref="char"/> stream.
+/// </summary>
+internal class UnixInputProcessor : InputProcessor<char>
+{
+    /// <inheritdoc />
+    public UnixInputProcessor (ConcurrentQueue<char> inputBuffer) : base (inputBuffer, new UnixKeyConverter ())
+    {
+        DriverName = "unix";
+    }
+
+    /// <inheritdoc />
+    protected override void Process (char input)
+    {
+        foreach (Tuple<char, char> released in Parser.ProcessInput (Tuple.Create (input, input)))
+        {
+            ProcessAfterParsing (released.Item2);
+        }
+
+    }
+
+    /// <inheritdoc />
+    protected override void ProcessAfterParsing (char input)
+    {
+        var key = KeyConverter.ToKey (input);
+
+        // If the key is not valid, we don't want to raise any events.
+        if (IsValidInput (key, out key))
+        {
+            OnKeyDown (key);
+            OnKeyUp (key);
+        }
+    }
+}

+ 20 - 0
Terminal.Gui/Drivers/V2/UnixKeyConverter.cs

@@ -0,0 +1,20 @@
+#nullable enable
+
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     <see cref="IKeyConverter{T}"/> capable of converting the
+///     unix native <see cref="char"/> class
+///     into Terminal.Gui shared <see cref="Key"/> representation
+///     (used by <see cref="View"/> etc).
+/// </summary>
+internal class UnixKeyConverter : IKeyConverter<char>
+{
+    /// <inheritdoc />
+    public Key ToKey (char value)
+    {
+        ConsoleKeyInfo adjustedInput = EscSeqUtils.MapChar (value);
+
+        return EscSeqUtils.MapKey (adjustedInput);
+    }
+}

+ 175 - 0
Terminal.Gui/Drivers/V2/UnixOutput.cs

@@ -0,0 +1,175 @@
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+namespace Terminal.Gui.Drivers;
+
+internal class UnixOutput : OutputBase, IConsoleOutput
+{
+    [StructLayout (LayoutKind.Sequential)]
+    private struct WinSize
+    {
+        public ushort ws_row;
+        public ushort ws_col;
+        public ushort ws_xpixel;
+        public ushort ws_ypixel;
+    }
+
+    private static readonly uint TIOCGWINSZ =
+        RuntimeInformation.IsOSPlatform (OSPlatform.OSX) ||
+        RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)
+            ? 0x40087468u  // Darwin/BSD
+            : 0x5413u;     // Linux
+
+    [DllImport ("libc", SetLastError = true)]
+    private static extern int ioctl (int fd, uint request, out WinSize ws);
+
+    // File descriptor for stdout
+    private const int STDOUT_FILENO = 1;
+
+    [DllImport ("libc")]
+    private static extern int write (int fd, byte [] buf, int n);
+
+    [DllImport ("libc", SetLastError = true)]
+    private static extern int dup (int fd);
+
+    /// <inheritdoc />
+    protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
+    {
+        if (Application.Force16Colors)
+        {
+            output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
+            output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+        }
+        else
+        {
+            EscSeqUtils.CSI_AppendForegroundColorRGB (
+                                                      output,
+                                                      attr.Foreground.R,
+                                                      attr.Foreground.G,
+                                                      attr.Foreground.B
+                                                     );
+
+            EscSeqUtils.CSI_AppendBackgroundColorRGB (
+                                                      output,
+                                                      attr.Background.R,
+                                                      attr.Background.G,
+                                                      attr.Background.B
+                                                     );
+            EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
+        }
+    }
+
+    /// <inheritdoc />
+    protected override void Write (StringBuilder output)
+    {
+        byte [] utf8 = Encoding.UTF8.GetBytes (output.ToString ());
+        // Write to stdout (fd 1)
+        write (STDOUT_FILENO, utf8, utf8.Length);
+    }
+
+    private Point? _lastCursorPosition;
+
+    /// <inheritdoc />
+    protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY)
+    {
+        if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == screenPositionX && _lastCursorPosition.Value.Y == screenPositionY)
+        {
+            return true;
+        }
+
+        _lastCursorPosition = new (screenPositionX, screenPositionY);
+
+        using var writer = CreateUnixStdoutWriter ();
+
+        // + 1 is needed because Unix is based on 1 instead of 0 and
+        EscSeqUtils.CSI_WriteCursorPosition (writer, screenPositionY + 1, screenPositionX + 1);
+
+        return true;
+    }
+
+    private TextWriter CreateUnixStdoutWriter ()
+    {
+        // duplicate stdout so we don’t mess with Console.Out’s FD
+        int fdCopy = dup (STDOUT_FILENO);
+
+        if (fdCopy == -1)
+        {
+            throw new IOException ("Failed to dup STDOUT_FILENO");
+        }
+
+        // wrap the raw fd into a SafeFileHandle
+        var handle = new SafeFileHandle (fdCopy, ownsHandle: true);
+
+        // create FileStream from the safe handle
+        var stream = new FileStream (handle, FileAccess.Write);
+
+        return new StreamWriter (stream)
+        {
+            AutoFlush = true
+        };
+    }
+
+    /// <inheritdoc />
+    public void Write (ReadOnlySpan<char> text)
+    {
+        if (!ConsoleDriver.RunningUnitTests)
+        {
+            byte [] utf8 = Encoding.UTF8.GetBytes (text.ToArray ());
+            // Write to stdout (fd 1)
+            write (STDOUT_FILENO, utf8, utf8.Length);
+        }
+    }
+
+    /// <inheritdoc />
+    public Size GetWindowSize ()
+    {
+        if (ConsoleDriver.RunningUnitTests)
+        {
+            // For unit tests, we return a default size.
+            return Size.Empty;
+        }
+
+        if (ioctl (1, TIOCGWINSZ, out WinSize ws) == 0)
+        {
+            if (ws.ws_col > 0 && ws.ws_row > 0)
+            {
+                return new (ws.ws_col, ws.ws_row);
+            }
+        }
+
+        return Size.Empty; // fallback
+    }
+
+    private EscSeqUtils.DECSCUSR_Style? _currentDecscusrStyle;
+
+    /// <inheritdoc cref="IConsoleOutput.SetCursorVisibility"/>
+    public override void SetCursorVisibility (CursorVisibility visibility)
+    {
+        if (visibility != CursorVisibility.Invisible)
+        {
+            if (_currentDecscusrStyle is null || _currentDecscusrStyle != (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF))
+            {
+                _currentDecscusrStyle = (EscSeqUtils.DECSCUSR_Style)(((int)visibility >> 24) & 0xFF);
+
+                Write (EscSeqUtils.CSI_SetCursorStyle ((EscSeqUtils.DECSCUSR_Style)_currentDecscusrStyle));
+            }
+
+            Write (EscSeqUtils.CSI_ShowCursor);
+        }
+        else
+        {
+            Write (EscSeqUtils.CSI_HideCursor);
+        }
+    }
+
+    /// <inheritdoc />
+    public void SetCursorPosition (int col, int row)
+    {
+        SetCursorPositionImpl (col, row);
+    }
+
+    /// <inheritdoc />
+    public void Dispose ()
+    {
+    }
+}

+ 4 - 1
Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs

@@ -13,7 +13,10 @@ internal class WindowsInputProcessor : InputProcessor<InputRecord>
     private readonly bool [] _lastWasPressed = new bool[4];
 
     /// <inheritdoc/>
-    public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { }
+    public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter ())
+    {
+        DriverName = "win";
+    }
 
     /// <inheritdoc/>
     protected override void Process (InputRecord inputEvent)

+ 1 - 0
Tests/UnitTests/Application/SynchronizatonContextTests.cs

@@ -30,6 +30,7 @@ public class SyncrhonizationContextTests
     [InlineData (typeof (CursesDriver))]
     [InlineData (typeof (ConsoleDriverFacade<WindowsConsole.InputRecord>), "v2win")]
     [InlineData (typeof (ConsoleDriverFacade<ConsoleKeyInfo>), "v2net")]
+    [InlineData (typeof (ConsoleDriverFacade<char>), "v2unix")]
     public void SynchronizationContext_Post (Type driverType, string driverName = null)
     {
         lock (_lockPost)

+ 14 - 3
Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs

@@ -103,16 +103,27 @@ public class AnsiKeyboardParserTests
         yield return new object [] { "\u001b[1;2P", Key.F1.WithShift };
         yield return new object [] { "\u001b[1;3Q", Key.F2.WithAlt };
         yield return new object [] { "\u001b[1;5R", Key.F3.WithCtrl };
-        
+
+        // Keys with Alt modifiers
+        yield return new object [] { "\u001ba", Key.A.WithAlt, true };
+        yield return new object [] { "\u001bA", Key.A.WithShift.WithAlt, true };
+        yield return new object [] { "\u001b1", Key.D1.WithAlt, true };
+
+        // Keys with Ctrl and Alt modifiers
+        yield return new object [] { "\u001b\u0001", Key.A.WithCtrl.WithAlt, true };
+        yield return new object [] { "\u001b\u001a", Key.Z.WithCtrl.WithAlt, true };
+
+        // Keys with Ctrl, Shift and Alt modifiers
+        yield return new object [] { "\u001b\u001f", Key.D7.WithCtrl.WithShift.WithAlt, true };
     }
 
     // Consolidated test for all keyboard events (e.g., arrow keys)
     [Theory]
     [MemberData (nameof (GetKeyboardTestData))]
-    public void ProcessKeyboardInput_ReturnsCorrectKey (string? input, Key? expectedKey)
+    public void ProcessKeyboardInput_ReturnsCorrectKey (string? input, Key? expectedKey, bool isLastMinute = false)
     {
         // Act
-        Key? result = _parser.IsKeyboard (input)?.GetKey (input);
+        Key? result = _parser.IsKeyboard (input, isLastMinute)?.GetKey (input);
 
         // Assert
         Assert.Equal (expectedKey, result); // Verify the returned key matches the expected one

+ 16 - 0
Tests/UnitTests/Input/EscSeqUtilsTests.cs

@@ -1538,6 +1538,22 @@ public class EscSeqUtilsTests
         Assert.Equal (expected, actual);
     }
 
+    [Theory]
+    [InlineData ('\u001B', KeyCode.Esc)]
+    [InlineData ('\r', KeyCode.Enter)]
+    [InlineData ('1', KeyCode.D1)]
+    [InlineData ('!', (KeyCode)'!')]
+    [InlineData ('a', KeyCode.A)]
+    [InlineData ('A', KeyCode.A | KeyCode.ShiftMask)]
+    public void MapChar_Returns_Modifiers_If_Needed (char ch, KeyCode keyCode)
+    {
+        ConsoleKeyInfo cki = EscSeqUtils.MapChar (ch);
+        Key key = EscSeqUtils.MapKey (cki);
+        Key expectedKey = keyCode;
+
+        Assert.Equal (key, expectedKey);
+    }
+
     private void ClearAll ()
     {
         EscSeqRequests.Clear ();