#nullable enable using System.Collections.Concurrent; using Microsoft.Extensions.Logging; namespace Terminal.Gui; /// /// Processes the queued input buffer contents - which must be of Type . /// Is responsible for and translating into common Terminal.Gui /// events and data models. /// public abstract class InputProcessor : IInputProcessor { /// /// 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; } /// /// Input buffer which will be drained from by this class. /// public ConcurrentQueue InputBuffer { get; } /// public IAnsiResponseParser GetParser () { return Parser; } private readonly MouseInterpreter _mouseInterpreter = new (); /// Event fired when a key is pressed down. This is a precursor to . public event EventHandler? KeyDown; /// Event fired when a terminal sequence read from input is not recognized and therefore ignored. public event EventHandler? AnsiSequenceSwallowed; /// /// Called when a key is pressed down. Fires the event. This is a precursor to /// . /// /// public void OnKeyDown (Key a) { Logging.Trace ($"{nameof (InputProcessor)} raised {a}"); KeyDown?.Invoke (this, a); } /// Event fired when a key is released. /// /// Drivers that do not support key release events will fire this event after processing is /// complete. /// public event EventHandler? KeyUp; /// Called when a key is released. Fires the event. /// /// Drivers that do not support key release events will call this method after processing /// is complete. /// /// public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } /// Event fired when a mouse event occurs. public event EventHandler? MouseEvent; /// Called when a mouse event occurs. Fires the event. /// 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); } } /// /// 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 InputProcessor (ConcurrentQueue inputBuffer, IKeyConverter 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)} ignored unrecognized response '{cur}'"); AnsiSequenceSwallowed?.Invoke (this, cur); return true; }; KeyConverter = keyConverter; } /// /// Drains the buffer, processing all available keystrokes /// public void ProcessQueue () { while (InputBuffer.TryDequeue (out T? input)) { Process (input); } foreach (T 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 (T input); /// /// Process the provided single input element - short-circuiting the /// stage of the processing. /// /// protected abstract void ProcessAfterParsing (T input); }