#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui; /// /// Parses mouse ansi escape sequences into /// including support for pressed, released and mouse wheel. /// public class AnsiMouseParser { // Regex patterns for button press/release, wheel scroll, and mouse position reporting private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled); /// /// Returns true if it is a mouse event /// /// /// public bool IsMouse (string? cur) { // Typically in this format // ESC [ < {button_code};{x_pos};{y_pos}{final_byte} return cur!.EndsWith ('M') || cur.EndsWith ('m'); } /// /// Parses a mouse ansi escape sequence into a mouse event. Returns null if input /// is not a mouse event or its syntax is not understood. /// /// /// public MouseEventArgs? ProcessMouseInput (string? input) { // Match mouse wheel events first Match match = _mouseEventPattern.Match (input!); if (match.Success) { int buttonCode = int.Parse (match.Groups [1].Value); // The top-left corner of the terminal corresponds to (1, 1) for both X (column) and Y (row) coordinates. // ANSI standards and terminal conventions historically treat screen positions as 1 - based. int x = int.Parse (match.Groups [2].Value) - 1; int y = int.Parse (match.Groups [3].Value) - 1; char terminator = match.Groups [4].Value.Single (); var m = new MouseEventArgs { Position = new (x, y), Flags = GetFlags (buttonCode, terminator) }; Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); return m; } // its some kind of odd mouse event that doesn't follow expected format? return null; } private static MouseFlags GetFlags (int buttonCode, char terminator) { MouseFlags buttonState = 0; switch (buttonCode) { case 0: case 8: case 16: case 24: case 32: case 36: case 40: case 48: case 56: buttonState = terminator == 'M' ? MouseFlags.Button1Pressed : MouseFlags.Button1Released; break; case 1: case 9: case 17: case 25: case 33: case 37: case 41: case 45: case 49: case 53: case 57: case 61: buttonState = terminator == 'M' ? MouseFlags.Button2Pressed : MouseFlags.Button2Released; break; case 2: case 10: case 14: case 18: case 22: case 26: case 30: case 34: case 42: case 46: case 50: case 54: case 58: case 62: buttonState = terminator == 'M' ? MouseFlags.Button3Pressed : MouseFlags.Button3Released; break; case 35: //// Needed for Windows OS //if (isButtonPressed && c == 'm' // && (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed // || lastMouseEvent.ButtonState == MouseFlags.Button2Pressed // || lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) { // switch (lastMouseEvent.ButtonState) { // case MouseFlags.Button1Pressed: // buttonState = MouseFlags.Button1Released; // break; // case MouseFlags.Button2Pressed: // buttonState = MouseFlags.Button2Released; // break; // case MouseFlags.Button3Pressed: // buttonState = MouseFlags.Button3Released; // break; // } //} else { // buttonState = MouseFlags.ReportMousePosition; //} //break; case 39: case 43: case 47: case 51: case 55: case 59: case 63: buttonState = MouseFlags.ReportMousePosition; break; case 64: buttonState = MouseFlags.WheeledUp; break; case 65: buttonState = MouseFlags.WheeledDown; break; case 68: case 72: case 80: buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp break; case 69: case 73: case 81: buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown break; } // Modifiers. switch (buttonCode) { case 8: case 9: case 10: case 43: buttonState |= MouseFlags.ButtonAlt; break; case 14: case 47: buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift; break; case 16: case 17: case 18: case 51: buttonState |= MouseFlags.ButtonCtrl; break; case 22: case 55: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; break; case 24: case 25: case 26: case 59: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; break; case 30: case 63: buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; break; case 32: case 33: case 34: buttonState |= MouseFlags.ReportMousePosition; break; case 36: case 37: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift; break; case 39: case 68: case 69: buttonState |= MouseFlags.ButtonShift; break; case 40: case 41: case 42: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt; break; case 45: case 46: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift; break; case 48: case 49: case 50: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl; break; case 53: case 54: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; break; case 56: case 57: case 58: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; break; case 61: case 62: buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; break; } return buttonState; } }