Files
Terminal.Gui/Terminal.Gui/Drivers/V2/InputProcessor.cs
BDisp 9d34e2792c Fixes #4250. Add a v2unix driver (#4251)
* Implement Unix driver

* Add more sequences related with macOS

* Helper method to map char to control keys

* Add DriverName property

* Fix error on Unix unit tests

* Fix Non-nullable property 'DriverName' must contain a non-null value when exiting constructor.

* Fix Ctrl being ignored in the range \u0001-\u001a

* Add unit test for the MapChar method

* Fix cursor visibility and cursor styles

* Avoid sometimes error when running gnome-terminal

* Captures Ctrl+Shift+Alt+D7

* Captures Ctrl+D4, Ctrl+D5, Ctrl+D6 and Ctrl+D7

* Add unit test for Ctrl+Shift+Alt+7

* Fix issue on Windows where sending AltGr+some key fail assertion

* Exclude Oem1 from assertion

* Fix regex pattern

* Remove driverName from the constructor

* Revert "Remove driverName from the constructor"

This reverts commit 004e9f9588.

* Remove driverName from the constructor

* Add generic CreateSubcomponents method avoiding redundancy

* Add v2unix profiles

* Replace with hexadecimal values

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
2025-10-02 08:51:05 -06:00

225 lines
7.3 KiB
C#

#nullable enable
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
namespace Terminal.Gui.Drivers;
/// <summary>
/// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>.
/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui
/// events and data models.
/// </summary>
public abstract class InputProcessor<T> : IInputProcessor
{
/// <summary>
/// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
/// </summary>
private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
internal AnsiResponseParser<T> Parser { get; } = new ();
/// <summary>
/// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g.
/// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all
/// internal library representations of Keys).
/// </summary>
public IKeyConverter<T> KeyConverter { get; }
/// <summary>
/// Input buffer which will be drained from by this class.
/// </summary>
public ConcurrentQueue<T> InputBuffer { get; }
/// <inheritdoc />
public string DriverName { get; init; }
/// <inheritdoc/>
public IAnsiResponseParser GetParser () { return Parser; }
private readonly MouseInterpreter _mouseInterpreter = new ();
/// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary>
public event EventHandler<Key>? KeyDown;
/// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary>
public event EventHandler<string>? AnsiSequenceSwallowed;
/// <summary>
/// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to
/// <see cref="OnKeyUp"/>.
/// </summary>
/// <param name="a"></param>
public void OnKeyDown (Key a)
{
Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}");
KeyDown?.Invoke (this, a);
}
/// <summary>Event fired when a key is released.</summary>
/// <remarks>
/// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is
/// complete.
/// </remarks>
public event EventHandler<Key>? KeyUp;
/// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary>
/// <remarks>
/// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing
/// is complete.
/// </remarks>
/// <param name="a"></param>
public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); }
/// <summary>Event fired when a mouse event occurs.</summary>
public event EventHandler<MouseEventArgs>? MouseEvent;
/// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary>
/// <param name="a"></param>
public void OnMouseEvent (MouseEventArgs a)
{
// Ensure ScreenPosition is set
a.ScreenPosition = a.Position;
foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
{
// Logging.Trace ($"Mouse Interpreter raising {e.Flags}");
// Pass on
MouseEvent?.Invoke (this, e);
}
}
/// <summary>
/// Constructs base instance including wiring all relevant
/// parser events and setting <see cref="InputBuffer"/> to
/// the provided thread safe input collection.
/// </summary>
/// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param>
/// <param name="keyConverter">
/// Key converter for translating driver specific
/// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.
/// </param>
protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
{
InputBuffer = inputBuffer;
Parser.HandleMouse = true;
Parser.Mouse += (s, e) => OnMouseEvent (e);
Parser.HandleKeyboard = true;
Parser.Keyboard += (s, k) =>
{
OnKeyDown (k);
OnKeyUp (k);
};
// TODO: For now handle all other escape codes with ignore
Parser.UnexpectedResponseHandler = str =>
{
var cur = new string (str.Select (k => k.Item1).ToArray ());
Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'");
AnsiSequenceSwallowed?.Invoke (this, cur);
return true;
};
KeyConverter = keyConverter;
}
/// <summary>
/// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
/// </summary>
public void ProcessQueue ()
{
while (InputBuffer.TryDequeue (out T? input))
{
Process (input);
}
foreach (T input in ReleaseParserHeldKeysIfStale ())
{
ProcessAfterParsing (input);
}
}
private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
{
if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
&& DateTime.Now - Parser.StateChangedAt > _escTimeout)
{
return Parser.Release ().Select (o => o.Item2);
}
return [];
}
/// <summary>
/// Process the provided single input element <paramref name="input"/>. This method
/// is called sequentially for each value read from <see cref="InputBuffer"/>.
/// </summary>
/// <param name="input"></param>
protected abstract void Process (T input);
/// <summary>
/// Process the provided single input element - short-circuiting the <see cref="Parser"/>
/// stage of the processing.
/// </summary>
/// <param name="input"></param>
protected abstract void ProcessAfterParsing (T input);
internal char _highSurrogate = '\0';
/// <inheritdoc />
public bool IsValidInput (Key key, out Key result)
{
result = key;
if (char.IsHighSurrogate ((char)key))
{
_highSurrogate = (char)key;
return false;
}
if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key))
{
result = (KeyCode)new Rune (_highSurrogate, (char)key).Value;
if (key.IsAlt)
{
result = result.WithAlt;
}
if (key.IsCtrl)
{
result = result.WithCtrl;
}
if (key.IsShift)
{
result = result.WithShift;
}
_highSurrogate = '\0';
return true;
}
if (char.IsSurrogate ((char)key))
{
return false;
}
if (_highSurrogate > 0)
{
_highSurrogate = '\0';
}
if (key.KeyCode == 0)
{
return false;
}
return true;
}
}