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