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

This commit is contained in:
Tig
2025-10-02 09:00:15 -06:00
22 changed files with 754 additions and 78 deletions

View File

@@ -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"
},

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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
};
}

View File

@@ -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 its 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;
}

View File

@@ -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 ()
private IMainLoopCoordinator CreateSubcomponents<T> (Func<IComponentFactory<T>> fallbackFactory)
{
ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ();
MainLoop<WindowsConsole.InputRecord> loop = new ();
ConcurrentQueue<T> inputBuffer = new ();
MainLoop<T> loop = new ();
IComponentFactory<WindowsConsole.InputRecord> cf;
IComponentFactory<T> cf;
if (_componentFactory != null)
if (_componentFactory is IComponentFactory<T> typedFactory)
{
cf = (IComponentFactory<WindowsConsole.InputRecord>)_componentFactory;
cf = typedFactory;
}
else
{
cf = new WindowsComponentFactory ();
cf = fallbackFactory ();
}
return new MainLoopCoordinator<WindowsConsole.InputRecord> (_timedEvents,
inputBuffer,
loop,
cf);
}
private IMainLoopCoordinator CreateNetSubcomponents ()
{
ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ();
MainLoop<ConsoleKeyInfo> loop = new ();
IComponentFactory<ConsoleKeyInfo> cf;
if (_componentFactory != null)
{
cf = (IComponentFactory<ConsoleKeyInfo>)_componentFactory;
}
else
{
cf = new NetComponentFactory ();
}
return new MainLoopCoordinator<ConsoleKeyInfo> (
_timedEvents,
inputBuffer,
loop,
cf);
return new MainLoopCoordinator<T> (_timedEvents, inputBuffer, loop, cf);
}
/// <inheritdoc/>

View File

@@ -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;
}

View File

@@ -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"/>.

View File

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

View File

@@ -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; }

View File

@@ -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 ();
}
}

View File

@@ -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)

View File

@@ -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
);
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_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);
}
}
}

View File

@@ -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 ();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 dont mess with Console.Outs 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 ()
{
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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 ();