diff --git a/Examples/UICatalog/Properties/launchSettings.json b/Examples/UICatalog/Properties/launchSettings.json index a20ddaf932..02062518fa 100644 --- a/Examples/UICatalog/Properties/launchSettings.json +++ b/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" }, diff --git a/Terminal.Gui/App/Application.Initialization.cs b/Terminal.Gui/App/Application.Initialization.cs index e79492fd5b..4b5f860962 100644 --- a/Terminal.Gui/App/Application.Initialization.cs +++ b/Terminal.Gui/App/Application.Initialization.cs @@ -217,7 +217,7 @@ public static (List, List) GetDriverTypes () List 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); diff --git a/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs b/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs index 1db3e87571..a9b16e90a8 100644 --- a/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs +++ b/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; } diff --git a/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs index ffb82fc095..988b584f14 100644 --- a/Terminal.Gui/Drivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs +++ b/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 /// @@ -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 }; } diff --git a/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs index 0a99fa8b5d..ddeb343727 100644 --- a/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/Drivers/EscSeqUtils/EscSeqUtils.cs @@ -1029,6 +1029,16 @@ Action continuousButtonPressedHandler //} } + /// + /// Helper to set the Control key states based on the char. + /// + /// The char value. + /// + public static ConsoleKeyInfo MapChar (char ch) + { + return MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false)); + } + /// /// Ensures a console key is mapped to one that works correctly with ANSI escape sequences. /// @@ -1131,6 +1141,17 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) 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 @@ internal static KeyCode MapKey (ConsoleKeyInfo keyInfo) { 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 @@ internal static KeyCode MapKey (ConsoleKeyInfo keyInfo) 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); } diff --git a/Terminal.Gui/Drivers/V2/ApplicationV2.cs b/Terminal.Gui/Drivers/V2/ApplicationV2.cs index a3964328f6..b3cf5238d9 100644 --- a/Terminal.Gui/Drivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/Drivers/V2/ApplicationV2.cs @@ -81,24 +81,29 @@ private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; - bool definetlyWin = (driverName?.Contains ("win") ?? false )|| _componentFactory is IComponentFactory; - bool definetlyNet = (driverName?.Contains ("net") ?? false ) || _componentFactory is IComponentFactory; + bool definetlyWin = (driverName?.Contains ("win") ?? false) || _componentFactory is IComponentFactory; + bool definetlyNet = (driverName?.Contains ("net") ?? false) || _componentFactory is IComponentFactory; + bool definetlyUnix = (driverName?.Contains ("unix") ?? false) || _componentFactory is IComponentFactory; 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 @@ private void CreateDriver (string? driverName) } } - private IMainLoopCoordinator CreateWindowsSubcomponents () - { - ConcurrentQueue inputBuffer = new (); - MainLoop loop = new (); - - IComponentFactory cf; - - if (_componentFactory != null) - { - cf = (IComponentFactory)_componentFactory; - } - else - { - cf = new WindowsComponentFactory (); - } - - return new MainLoopCoordinator (_timedEvents, - inputBuffer, - loop, - cf); - } - - private IMainLoopCoordinator CreateNetSubcomponents () + private IMainLoopCoordinator CreateSubcomponents (Func> fallbackFactory) { - ConcurrentQueue inputBuffer = new (); - MainLoop loop = new (); + ConcurrentQueue inputBuffer = new (); + MainLoop loop = new (); - IComponentFactory cf; + IComponentFactory cf; - if (_componentFactory != null) + if (_componentFactory is IComponentFactory typedFactory) { - cf = (IComponentFactory)_componentFactory; + cf = typedFactory; } else { - cf = new NetComponentFactory (); + cf = fallbackFactory (); } - return new MainLoopCoordinator ( - _timedEvents, - inputBuffer, - loop, - cf); + return new MainLoopCoordinator (_timedEvents, inputBuffer, loop, cf); } /// diff --git a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs index 893164d0df..c705a3b7eb 100644 --- a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs @@ -260,16 +260,7 @@ public void ClearContents () /// 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; } diff --git a/Terminal.Gui/Drivers/V2/IInputProcessor.cs b/Terminal.Gui/Drivers/V2/IInputProcessor.cs index 2c990db3fc..8366788af2 100644 --- a/Terminal.Gui/Drivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/IInputProcessor.cs @@ -25,6 +25,11 @@ public interface IInputProcessor /// Event fired when a mouse event occurs. event EventHandler? MouseEvent; + /// + /// Gets the name of the driver associated with this input processor. + /// + string DriverName { get; init; } + /// /// Called when a key is pressed down. Fires the event. This is a precursor to /// . diff --git a/Terminal.Gui/Drivers/V2/IUnixInput.cs b/Terminal.Gui/Drivers/V2/IUnixInput.cs new file mode 100644 index 0000000000..23755f0c2c --- /dev/null +++ b/Terminal.Gui/Drivers/V2/IUnixInput.cs @@ -0,0 +1,3 @@ +namespace Terminal.Gui.Drivers; + +internal interface IUnixInput : IConsoleInput; diff --git a/Terminal.Gui/Drivers/V2/InputProcessor.cs b/Terminal.Gui/Drivers/V2/InputProcessor.cs index 04a4e3b6c2..667d4d267e 100644 --- a/Terminal.Gui/Drivers/V2/InputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/InputProcessor.cs @@ -30,6 +30,9 @@ public abstract class InputProcessor : IInputProcessor /// public ConcurrentQueue InputBuffer { get; } + /// + public string DriverName { get; init; } + /// public IAnsiResponseParser GetParser () { return Parser; } diff --git a/Terminal.Gui/Drivers/V2/NetInputProcessor.cs b/Terminal.Gui/Drivers/V2/NetInputProcessor.cs index 36c885e355..f2a2d1aca1 100644 --- a/Terminal.Gui/Drivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/NetInputProcessor.cs @@ -20,7 +20,10 @@ public class NetInputProcessor : InputProcessor #pragma warning restore CA2211 /// - public NetInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new NetKeyConverter ()) { } + public NetInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new NetKeyConverter ()) + { + DriverName = "net"; + } /// protected override void Process (ConsoleKeyInfo consoleKeyInfo) diff --git a/Terminal.Gui/Drivers/V2/UnixComponentFactory.cs b/Terminal.Gui/Drivers/V2/UnixComponentFactory.cs new file mode 100644 index 0000000000..c2de426968 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/UnixComponentFactory.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation for native unix console I/O i.e. v2unix. +/// This factory creates instances of internal classes , etc. +/// +public class UnixComponentFactory : ComponentFactory +{ + /// + public override IConsoleInput CreateInput () + { + return new UnixInput (); + } + + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + { + return new UnixInputProcessor (inputBuffer); + } + + /// + public override IConsoleOutput CreateOutput () + { + return new UnixOutput (); + } +} diff --git a/Terminal.Gui/Drivers/V2/UnixInput.cs b/Terminal.Gui/Drivers/V2/UnixInput.cs new file mode 100644 index 0000000000..793553e33a --- /dev/null +++ b/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, 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" + } + + /// Condition on which to wake up from file descriptor activity. These match the Linux/BSD poll definitions. + [Flags] + private enum Condition : short + { + /// There is data to read + PollIn = 1, + + /// There is urgent data to read + PollPri = 2, + + /// Writing to the specified descriptor will not block + PollOut = 4, + + /// Error condition on output + PollErr = 8, + + /// Hang-up on output + PollHup = 16, + + /// File descriptor is not open. + 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}"; + } + + /// + 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); + } + } + + /// + protected override IEnumerable 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); + } + } + } + + /// + 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); + } + } +} diff --git a/Terminal.Gui/Drivers/V2/UnixInputProcessor.cs b/Terminal.Gui/Drivers/V2/UnixInputProcessor.cs new file mode 100644 index 0000000000..8187702d65 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/UnixInputProcessor.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Input processor for , deals in stream. +/// +internal class UnixInputProcessor : InputProcessor +{ + /// + public UnixInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new UnixKeyConverter ()) + { + DriverName = "unix"; + } + + /// + protected override void Process (char input) + { + foreach (Tuple released in Parser.ProcessInput (Tuple.Create (input, input))) + { + ProcessAfterParsing (released.Item2); + } + + } + + /// + 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); + } + } +} diff --git a/Terminal.Gui/Drivers/V2/UnixKeyConverter.cs b/Terminal.Gui/Drivers/V2/UnixKeyConverter.cs new file mode 100644 index 0000000000..cdaa385373 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/UnixKeyConverter.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Terminal.Gui.Drivers; + +/// +/// capable of converting the +/// unix native class +/// into Terminal.Gui shared representation +/// (used by etc). +/// +internal class UnixKeyConverter : IKeyConverter +{ + /// + public Key ToKey (char value) + { + ConsoleKeyInfo adjustedInput = EscSeqUtils.MapChar (value); + + return EscSeqUtils.MapKey (adjustedInput); + } +} diff --git a/Terminal.Gui/Drivers/V2/UnixOutput.cs b/Terminal.Gui/Drivers/V2/UnixOutput.cs new file mode 100644 index 0000000000..9643223d31 --- /dev/null +++ b/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); + + /// + 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); + } + } + + /// + 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; + + /// + 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 + }; + } + + /// + public void Write (ReadOnlySpan text) + { + if (!ConsoleDriver.RunningUnitTests) + { + byte [] utf8 = Encoding.UTF8.GetBytes (text.ToArray ()); + // Write to stdout (fd 1) + write (STDOUT_FILENO, utf8, utf8.Length); + } + } + + /// + 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; + + /// + 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); + } + } + + /// + public void SetCursorPosition (int col, int row) + { + SetCursorPositionImpl (col, row); + } + + /// + public void Dispose () + { + } +} diff --git a/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs index aa50b20b43..54e8c259c9 100644 --- a/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs @@ -13,7 +13,10 @@ internal class WindowsInputProcessor : InputProcessor private readonly bool [] _lastWasPressed = new bool[4]; /// - public WindowsInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { } + public WindowsInputProcessor (ConcurrentQueue inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) + { + DriverName = "win"; + } /// protected override void Process (InputRecord inputEvent) diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 86f1380279..0a3c1120f8 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -30,6 +30,7 @@ public void SynchronizationContext_CreateCopy () [InlineData (typeof (CursesDriver))] [InlineData (typeof (ConsoleDriverFacade), "v2win")] [InlineData (typeof (ConsoleDriverFacade), "v2net")] + [InlineData (typeof (ConsoleDriverFacade), "v2unix")] public void SynchronizationContext_Post (Type driverType, string driverName = null) { lock (_lockPost) diff --git a/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs index 4e178bdc69..09fd448c25 100644 --- a/Tests/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs +++ b/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 diff --git a/Tests/UnitTests/Input/EscSeqUtilsTests.cs b/Tests/UnitTests/Input/EscSeqUtilsTests.cs index 6daf4bd1ec..4f32511593 100644 --- a/Tests/UnitTests/Input/EscSeqUtilsTests.cs +++ b/Tests/UnitTests/Input/EscSeqUtilsTests.cs @@ -1538,6 +1538,22 @@ public void CSI_WriteCursorPosition_ReturnsCorrectEscSeq (int row, int col, stri 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 ();