AnsiMouseParser.cs 7.8 KB

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