AnsiMouseParser.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. using System.Text.RegularExpressions;
  2. namespace Terminal.Gui.Drivers;
  3. /// <summary>
  4. /// Parses mouse ansi escape sequences into <see cref="MouseEventArgs"/>
  5. /// including support for pressed, released and mouse wheel.
  6. /// </summary>
  7. public class AnsiMouseParser
  8. {
  9. // Regex patterns for button press/release, wheel scroll, and mouse position reporting
  10. private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled);
  11. /// <summary>
  12. /// Returns true if it is a mouse event
  13. /// </summary>
  14. /// <param name="cur"></param>
  15. /// <returns></returns>
  16. public bool IsMouse (string? cur)
  17. {
  18. // Typically in this format
  19. // ESC [ < {button_code};{x_pos};{y_pos}{final_byte}
  20. return cur!.EndsWith ('M') || cur.EndsWith ('m');
  21. }
  22. /// <summary>
  23. /// Parses a mouse ansi escape sequence into a mouse event. Returns null if input
  24. /// is not a mouse event or its syntax is not understood.
  25. /// </summary>
  26. /// <param name="input"></param>
  27. /// <returns></returns>
  28. public MouseEventArgs? ProcessMouseInput (string? input)
  29. {
  30. // Match mouse wheel events first
  31. Match match = _mouseEventPattern.Match (input!);
  32. if (match.Success)
  33. {
  34. int buttonCode = int.Parse (match.Groups [1].Value);
  35. // The top-left corner of the terminal corresponds to (1, 1) for both X (column) and Y (row) coordinates.
  36. // ANSI standards and terminal conventions historically treat screen positions as 1 - based.
  37. int x = int.Parse (match.Groups [2].Value) - 1;
  38. int y = int.Parse (match.Groups [3].Value) - 1;
  39. char terminator = match.Groups [4].Value.Single ();
  40. var m = new MouseEventArgs
  41. {
  42. Position = new (x, y),
  43. Flags = GetFlags (buttonCode, terminator)
  44. };
  45. //Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}");
  46. return m;
  47. }
  48. // its some kind of odd mouse event that doesn't follow expected format?
  49. return null;
  50. }
  51. private static MouseFlags GetFlags (int buttonCode, char terminator)
  52. {
  53. MouseFlags buttonState = 0;
  54. switch (buttonCode)
  55. {
  56. case 0:
  57. case 8:
  58. case 16:
  59. case 24:
  60. case 32:
  61. case 36:
  62. case 40:
  63. case 48:
  64. case 56:
  65. buttonState = terminator == 'M'
  66. ? MouseFlags.Button1Pressed
  67. : MouseFlags.Button1Released;
  68. break;
  69. case 1:
  70. case 9:
  71. case 17:
  72. case 25:
  73. case 33:
  74. case 37:
  75. case 41:
  76. case 45:
  77. case 49:
  78. case 53:
  79. case 57:
  80. case 61:
  81. buttonState = terminator == 'M'
  82. ? MouseFlags.Button2Pressed
  83. : MouseFlags.Button2Released;
  84. break;
  85. case 2:
  86. case 10:
  87. case 14:
  88. case 18:
  89. case 22:
  90. case 26:
  91. case 30:
  92. case 34:
  93. case 42:
  94. case 46:
  95. case 50:
  96. case 54:
  97. case 58:
  98. case 62:
  99. buttonState = terminator == 'M'
  100. ? MouseFlags.Button3Pressed
  101. : MouseFlags.Button3Released;
  102. break;
  103. case 35:
  104. //// Needed for Windows OS
  105. //if (isButtonPressed && c == 'm'
  106. // && (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed
  107. // || lastMouseEvent.ButtonState == MouseFlags.Button2Pressed
  108. // || lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) {
  109. // switch (lastMouseEvent.ButtonState) {
  110. // case MouseFlags.Button1Pressed:
  111. // buttonState = MouseFlags.Button1Released;
  112. // break;
  113. // case MouseFlags.Button2Pressed:
  114. // buttonState = MouseFlags.Button2Released;
  115. // break;
  116. // case MouseFlags.Button3Pressed:
  117. // buttonState = MouseFlags.Button3Released;
  118. // break;
  119. // }
  120. //} else {
  121. // buttonState = MouseFlags.ReportMousePosition;
  122. //}
  123. //break;
  124. case 39:
  125. case 43:
  126. case 47:
  127. case 51:
  128. case 55:
  129. case 59:
  130. case 63:
  131. buttonState = MouseFlags.ReportMousePosition;
  132. break;
  133. case 64:
  134. buttonState = MouseFlags.WheeledUp;
  135. break;
  136. case 65:
  137. buttonState = MouseFlags.WheeledDown;
  138. break;
  139. case 68:
  140. case 72:
  141. case 80:
  142. buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp
  143. break;
  144. case 69:
  145. case 73:
  146. case 81:
  147. buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown
  148. break;
  149. }
  150. // Modifiers.
  151. switch (buttonCode)
  152. {
  153. case 8:
  154. case 9:
  155. case 10:
  156. case 43:
  157. buttonState |= MouseFlags.ButtonAlt;
  158. break;
  159. case 14:
  160. case 47:
  161. buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
  162. break;
  163. case 16:
  164. case 17:
  165. case 18:
  166. case 51:
  167. buttonState |= MouseFlags.ButtonCtrl;
  168. break;
  169. case 22:
  170. case 55:
  171. buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
  172. break;
  173. case 24:
  174. case 25:
  175. case 26:
  176. case 59:
  177. buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
  178. break;
  179. case 30:
  180. case 63:
  181. buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
  182. break;
  183. case 32:
  184. case 33:
  185. case 34:
  186. buttonState |= MouseFlags.ReportMousePosition;
  187. break;
  188. case 36:
  189. case 37:
  190. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift;
  191. break;
  192. case 39:
  193. case 68:
  194. case 69:
  195. buttonState |= MouseFlags.ButtonShift;
  196. break;
  197. case 40:
  198. case 41:
  199. case 42:
  200. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt;
  201. break;
  202. case 45:
  203. case 46:
  204. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift;
  205. break;
  206. case 48:
  207. case 49:
  208. case 50:
  209. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl;
  210. break;
  211. case 53:
  212. case 54:
  213. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift;
  214. break;
  215. case 56:
  216. case 57:
  217. case 58:
  218. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt;
  219. break;
  220. case 61:
  221. case 62:
  222. buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt;
  223. break;
  224. }
  225. return buttonState;
  226. }
  227. }