#nullable enable
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
namespace Terminal.Gui.Drivers;
///
/// Processes the queued input queue contents - which must be of Type .
/// Is responsible for and translating into common Terminal.Gui
/// events and data models. Runs on the main loop thread.
///
public abstract class InputProcessorImpl : IInputProcessor, IDisposable where TInputRecord : struct
{
///
/// Constructs base instance including wiring all relevant
/// parser events and setting to
/// the provided thread safe input collection.
///
/// The collection that will be populated with new input (see )
///
/// Key converter for translating driver specific
/// class into Terminal.Gui .
///
protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyConverter keyConverter)
{
InputQueue = inputBuffer;
Parser.HandleMouse = true;
Parser.Mouse += (s, e) => RaiseMouseEvent (e);
Parser.HandleKeyboard = true;
Parser.Keyboard += (s, k) =>
{
RaiseKeyDownEvent (k);
RaiseKeyUpEvent (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 (InputProcessorImpl)} ignored unrecognized response '{cur}'");
AnsiSequenceSwallowed?.Invoke (this, cur);
return true;
};
KeyConverter = keyConverter;
}
///
/// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
///
private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
internal AnsiResponseParser Parser { get; } = new ();
///
/// Class responsible for translating the driver specific native input class e.g.
/// into the Terminal.Gui class (used for all
/// internal library representations of Keys).
///
public IKeyConverter KeyConverter { get; }
///
/// The input queue which is filled by implementations running on the input thread.
/// Implementations of this class should dequeue from this queue in on the main loop thread.
///
public ConcurrentQueue InputQueue { get; }
///
public string? DriverName { get; init; }
///
public IAnsiResponseParser GetParser () { return Parser; }
private readonly MouseInterpreter _mouseInterpreter = new ();
///
public event EventHandler? KeyDown;
///
public event EventHandler? AnsiSequenceSwallowed;
///
public void RaiseKeyDownEvent (Key a)
{
KeyDown?.Invoke (this, a);
}
///
public event EventHandler? KeyUp;
///
public void RaiseKeyUpEvent (Key a) { KeyUp?.Invoke (this, a); }
///
///
///
public IInput? InputImpl { get; set; } // Set by MainLoopCoordinator
///
public void EnqueueKeyDownEvent (Key key)
{
// Convert Key → TInputRecord
TInputRecord inputRecord = KeyConverter.ToKeyInfo (key);
// If input supports testing, use InputImplPeek/Read pipeline
// which runs on the input thread.
if (InputImpl is ITestableInput testableInput)
{
testableInput.AddInput (inputRecord);
}
}
///
public void EnqueueKeyUpEvent (Key key)
{
// TODO: Determine if we can still support this on Windows
throw new NotImplementedException ();
}
///
public event EventHandler? MouseEvent;
///
public virtual void EnqueueMouseEvent (MouseEventArgs mouseEvent)
{
// Base implementation: For drivers where TInputRecord cannot represent mouse events
// (e.g., ConsoleKeyInfo), derived classes should override this method.
// See WindowsInputProcessor for an example implementation that converts MouseEventArgs
// to InputRecord and enqueues it.
Logging.Logger.LogWarning (
$"{DriverName ?? "Unknown"} driver's InputProcessor does not support EnqueueMouseEvent. " +
"Override this method to enable mouse event enqueueing for testing.");
}
///
public void RaiseMouseEvent (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);
}
}
///
public void ProcessQueue ()
{
while (InputQueue.TryDequeue (out TInputRecord input))
{
Process (input);
}
foreach (TInputRecord input in ReleaseParserHeldKeysIfStale ())
{
ProcessAfterParsing (input);
}
}
private IEnumerable ReleaseParserHeldKeysIfStale ()
{
if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
&& DateTime.Now - Parser.StateChangedAt > _escTimeout)
{
return Parser.Release ().Select (o => o.Item2);
}
return [];
}
///
/// Process the provided single input element . This method
/// is called sequentially for each value read from .
///
///
protected abstract void Process (TInputRecord input);
///
/// Process the provided single input element - short-circuiting the
/// stage of the processing.
///
///
protected virtual void ProcessAfterParsing (TInputRecord 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))
{
RaiseKeyDownEvent (key);
RaiseKeyUpEvent (key);
}
}
private char _highSurrogate = '\0';
///
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;
}
///
public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
///
public void Dispose () { ExternalCancellationTokenSource?.Dispose (); }
}