1use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct MouseMsg {
27 pub x: u16,
29 pub y: u16,
31 pub shift: bool,
33 pub alt: bool,
35 pub ctrl: bool,
37 pub action: MouseAction,
39 pub button: MouseButton,
41}
42
43impl MouseMsg {
44 pub fn is_wheel(&self) -> bool {
46 matches!(
47 self.button,
48 MouseButton::WheelUp
49 | MouseButton::WheelDown
50 | MouseButton::WheelLeft
51 | MouseButton::WheelRight
52 )
53 }
54}
55
56impl Default for MouseMsg {
57 fn default() -> Self {
58 Self {
59 x: 0,
60 y: 0,
61 shift: false,
62 alt: false,
63 ctrl: false,
64 action: MouseAction::Press,
65 button: MouseButton::None,
66 }
67 }
68}
69
70impl fmt::Display for MouseMsg {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 if self.ctrl {
73 write!(f, "ctrl+")?;
74 }
75 if self.alt {
76 write!(f, "alt+")?;
77 }
78 if self.shift {
79 write!(f, "shift+")?;
80 }
81
82 if self.button == MouseButton::None {
83 if self.action == MouseAction::Motion || self.action == MouseAction::Release {
84 write!(f, "{}", self.action)?;
85 } else {
86 write!(f, "unknown")?;
87 }
88 } else if self.is_wheel() {
89 write!(f, "{}", self.button)?;
90 } else {
91 write!(f, "{}", self.button)?;
92 write!(f, " {}", self.action)?;
93 }
94 Ok(())
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
100pub enum MouseAction {
101 #[default]
103 Press,
104 Release,
106 Motion,
108}
109
110impl fmt::Display for MouseAction {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 let name = match self {
113 MouseAction::Press => "press",
114 MouseAction::Release => "release",
115 MouseAction::Motion => "motion",
116 };
117 write!(f, "{}", name)
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
125pub enum MouseButton {
126 #[default]
128 None,
129 Left,
131 Middle,
133 Right,
135 WheelUp,
137 WheelDown,
139 WheelLeft,
141 WheelRight,
143 Backward,
145 Forward,
147 Button10,
149 Button11,
151}
152
153impl fmt::Display for MouseButton {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 let name = match self {
156 MouseButton::None => "none",
157 MouseButton::Left => "left",
158 MouseButton::Middle => "middle",
159 MouseButton::Right => "right",
160 MouseButton::WheelUp => "wheel up",
161 MouseButton::WheelDown => "wheel down",
162 MouseButton::WheelLeft => "wheel left",
163 MouseButton::WheelRight => "wheel right",
164 MouseButton::Backward => "backward",
165 MouseButton::Forward => "forward",
166 MouseButton::Button10 => "button 10",
167 MouseButton::Button11 => "button 11",
168 };
169 write!(f, "{}", name)
170 }
171}
172
173pub fn from_crossterm_mouse(event: crossterm::event::MouseEvent) -> MouseMsg {
175 use crossterm::event::{MouseButton as CtButton, MouseEventKind};
176
177 let action = match event.kind {
178 MouseEventKind::Down(_) => MouseAction::Press,
179 MouseEventKind::Up(_) => MouseAction::Release,
180 MouseEventKind::Drag(_) => MouseAction::Motion,
181 MouseEventKind::Moved => MouseAction::Motion,
182 MouseEventKind::ScrollUp => MouseAction::Press,
183 MouseEventKind::ScrollDown => MouseAction::Press,
184 MouseEventKind::ScrollLeft => MouseAction::Press,
185 MouseEventKind::ScrollRight => MouseAction::Press,
186 };
187
188 let button = match event.kind {
189 MouseEventKind::Down(b) | MouseEventKind::Up(b) | MouseEventKind::Drag(b) => match b {
190 CtButton::Left => MouseButton::Left,
191 CtButton::Right => MouseButton::Right,
192 CtButton::Middle => MouseButton::Middle,
193 },
194 MouseEventKind::ScrollUp => MouseButton::WheelUp,
195 MouseEventKind::ScrollDown => MouseButton::WheelDown,
196 MouseEventKind::ScrollLeft => MouseButton::WheelLeft,
197 MouseEventKind::ScrollRight => MouseButton::WheelRight,
198 MouseEventKind::Moved => MouseButton::None,
199 };
200
201 MouseMsg {
202 x: event.column,
203 y: event.row,
204 shift: event
205 .modifiers
206 .contains(crossterm::event::KeyModifiers::SHIFT),
207 alt: event
208 .modifiers
209 .contains(crossterm::event::KeyModifiers::ALT),
210 ctrl: event
211 .modifiers
212 .contains(crossterm::event::KeyModifiers::CONTROL),
213 action,
214 button,
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MouseParseError {
221 UnsupportedSequence,
223 InvalidFormat,
225 InvalidNumber,
227 CoordinateUnderflow,
229}
230
231impl fmt::Display for MouseParseError {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 let msg = match self {
234 MouseParseError::UnsupportedSequence => "unsupported mouse sequence",
235 MouseParseError::InvalidFormat => "invalid mouse sequence format",
236 MouseParseError::InvalidNumber => "invalid numeric value in mouse sequence",
237 MouseParseError::CoordinateUnderflow => "mouse coordinates underflowed",
238 };
239 write!(f, "{}", msg)
240 }
241}
242
243impl std::error::Error for MouseParseError {}
244
245#[derive(Debug, Clone, Copy)]
246struct ParsedMouse {
247 button: MouseButton,
248 action: MouseAction,
249 shift: bool,
250 alt: bool,
251 ctrl: bool,
252}
253
254fn is_wheel_button(button: MouseButton) -> bool {
255 matches!(
256 button,
257 MouseButton::WheelUp
258 | MouseButton::WheelDown
259 | MouseButton::WheelLeft
260 | MouseButton::WheelRight
261 )
262}
263
264fn parse_mouse_button(encoded: u16, is_sgr: bool) -> ParsedMouse {
265 let mut e = encoded;
266 if !is_sgr {
267 e = e.saturating_sub(32);
268 }
269
270 const BIT_SHIFT: u16 = 0b0000_0100;
271 const BIT_ALT: u16 = 0b0000_1000;
272 const BIT_CTRL: u16 = 0b0001_0000;
273 const BIT_MOTION: u16 = 0b0010_0000;
274 const BIT_WHEEL: u16 = 0b0100_0000;
275 const BIT_ADD: u16 = 0b1000_0000;
276 const BITS_MASK: u16 = 0b0000_0011;
277
278 let mut action = MouseAction::Press;
279 let button = if e & BIT_ADD != 0 {
280 match e & BITS_MASK {
281 0 => MouseButton::Backward,
282 1 => MouseButton::Forward,
283 2 => MouseButton::Button10,
284 _ => MouseButton::Button11,
285 }
286 } else if e & BIT_WHEEL != 0 {
287 match e & BITS_MASK {
288 0 => MouseButton::WheelUp,
289 1 => MouseButton::WheelDown,
290 2 => MouseButton::WheelLeft,
291 _ => MouseButton::WheelRight,
292 }
293 } else {
294 match e & BITS_MASK {
295 0 => MouseButton::Left,
296 1 => MouseButton::Middle,
297 2 => MouseButton::Right,
298 _ => {
299 action = MouseAction::Release;
300 MouseButton::None
301 }
302 }
303 };
304
305 if e & BIT_MOTION != 0 && !is_wheel_button(button) {
306 action = MouseAction::Motion;
307 }
308
309 ParsedMouse {
310 button,
311 action,
312 shift: e & BIT_SHIFT != 0,
313 alt: e & BIT_ALT != 0,
314 ctrl: e & BIT_CTRL != 0,
315 }
316}
317
318fn parse_x10_mouse_event(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
319 if buf.len() < 6 {
320 return Err(MouseParseError::InvalidFormat);
321 }
322
323 let parsed = parse_mouse_button(buf[3] as u16, false);
324
325 let x = i32::from(buf[4]) - 32 - 1;
326 let y = i32::from(buf[5]) - 32 - 1;
327 if x < 0 || y < 0 {
328 return Err(MouseParseError::CoordinateUnderflow);
329 }
330
331 Ok(MouseMsg {
332 x: x as u16,
333 y: y as u16,
334 shift: parsed.shift,
335 alt: parsed.alt,
336 ctrl: parsed.ctrl,
337 action: parsed.action,
338 button: parsed.button,
339 })
340}
341
342fn parse_sgr_mouse_event(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
343 if !buf.starts_with(b"\x1b[<") {
344 return Err(MouseParseError::InvalidFormat);
345 }
346
347 let mut nums = [0u16; 3];
348 let mut idx = 0usize;
349 let mut current: u16 = 0;
350 let mut has_digit = false;
351 let mut release = false;
352
353 for &b in &buf[3..] {
354 match b {
355 b'0'..=b'9' => {
356 current = current
357 .checked_mul(10)
358 .and_then(|v| v.checked_add(u16::from(b - b'0')))
359 .ok_or(MouseParseError::InvalidNumber)?;
360 has_digit = true;
361 }
362 b';' => {
363 if !has_digit || idx >= nums.len() {
364 return Err(MouseParseError::InvalidFormat);
365 }
366 nums[idx] = current;
367 idx += 1;
368 current = 0;
369 has_digit = false;
370 }
371 b'M' | b'm' => {
372 if !has_digit || idx != 2 {
373 return Err(MouseParseError::InvalidFormat);
374 }
375 nums[idx] = current;
376 release = b == b'm';
377 break;
378 }
379 _ => return Err(MouseParseError::InvalidFormat),
380 }
381 }
382
383 let mut parsed = parse_mouse_button(nums[0], true);
384 if release && parsed.action != MouseAction::Motion && !is_wheel_button(parsed.button) {
385 parsed.action = MouseAction::Release;
386 }
387
388 if nums[1] == 0 || nums[2] == 0 {
389 return Err(MouseParseError::CoordinateUnderflow);
390 }
391
392 Ok(MouseMsg {
393 x: nums[1] - 1,
394 y: nums[2] - 1,
395 shift: parsed.shift,
396 alt: parsed.alt,
397 ctrl: parsed.ctrl,
398 action: parsed.action,
399 button: parsed.button,
400 })
401}
402
403pub fn parse_mouse_event_sequence(buf: &[u8]) -> Result<MouseMsg, MouseParseError> {
405 if buf.starts_with(b"\x1b[<") {
406 parse_sgr_mouse_event(buf)
407 } else if buf.starts_with(b"\x1b[M") {
408 parse_x10_mouse_event(buf)
409 } else {
410 Err(MouseParseError::UnsupportedSequence)
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use proptest::prelude::*;
418
419 fn sgr_sequence_bytes(encoded: u16, x: u16, y: u16, release: bool) -> Vec<u8> {
420 let suffix = if release { 'm' } else { 'M' };
421 format!("\x1b[<{};{};{}{}", encoded, x, y, suffix).into_bytes()
422 }
423
424 fn expected_sgr_mouse(encoded: u16, x: u16, y: u16, release: bool) -> MouseMsg {
425 let mut parsed = parse_mouse_button(encoded, true);
426 if release && parsed.action != MouseAction::Motion && !is_wheel_button(parsed.button) {
427 parsed.action = MouseAction::Release;
428 }
429 MouseMsg {
430 x: x - 1,
431 y: y - 1,
432 shift: parsed.shift,
433 alt: parsed.alt,
434 ctrl: parsed.ctrl,
435 action: parsed.action,
436 button: parsed.button,
437 }
438 }
439
440 fn x10_sequence_bytes(encoded: u8, x: u16, y: u16) -> [u8; 6] {
441 let x_byte = (x + 33) as u8;
442 let y_byte = (y + 33) as u8;
443 [0x1b, b'[', b'M', encoded, x_byte, y_byte]
444 }
445
446 fn expected_x10_mouse(encoded: u8, x: u16, y: u16) -> MouseMsg {
447 let parsed = parse_mouse_button(u16::from(encoded), false);
448 MouseMsg {
449 x,
450 y,
451 shift: parsed.shift,
452 alt: parsed.alt,
453 ctrl: parsed.ctrl,
454 action: parsed.action,
455 button: parsed.button,
456 }
457 }
458
459 #[test]
460 fn test_mouse_msg_display() {
461 let mouse = MouseMsg {
462 x: 10,
463 y: 20,
464 shift: false,
465 alt: false,
466 ctrl: false,
467 action: MouseAction::Press,
468 button: MouseButton::Left,
469 };
470 assert_eq!(mouse.to_string(), "left press");
471
472 let mouse = MouseMsg {
473 x: 10,
474 y: 20,
475 shift: false,
476 alt: false,
477 ctrl: true,
478 action: MouseAction::Press,
479 button: MouseButton::Left,
480 };
481 assert_eq!(mouse.to_string(), "ctrl+left press");
482 }
483
484 #[test]
485 fn test_mouse_is_wheel() {
486 let mouse = MouseMsg {
487 button: MouseButton::WheelUp,
488 ..Default::default()
489 };
490 assert!(mouse.is_wheel());
491
492 let mouse = MouseMsg {
493 button: MouseButton::Left,
494 ..Default::default()
495 };
496 assert!(!mouse.is_wheel());
497 }
498
499 #[test]
500 fn test_mouse_button_display() {
501 assert_eq!(MouseButton::Left.to_string(), "left");
502 assert_eq!(MouseButton::WheelUp.to_string(), "wheel up");
503 }
504
505 #[test]
506 fn test_mouse_action_display() {
507 assert_eq!(MouseAction::Press.to_string(), "press");
508 assert_eq!(MouseAction::Release.to_string(), "release");
509 assert_eq!(MouseAction::Motion.to_string(), "motion");
510 }
511
512 proptest! {
513 #[test]
514 fn prop_parse_sgr_mouse_roundtrip(
515 encoded in 0u16..=255,
516 x in 1u16..=2000,
517 y in 1u16..=2000,
518 release in any::<bool>(),
519 ) {
520 let buf = sgr_sequence_bytes(encoded, x, y, release);
521 let msg = parse_mouse_event_sequence(&buf).unwrap();
522 let expected = expected_sgr_mouse(encoded, x, y, release);
523 prop_assert_eq!(msg, expected);
524 }
525
526 #[test]
527 fn prop_parse_x10_mouse_roundtrip(
528 encoded in 32u8..=255,
529 x in 0u16..=222,
530 y in 0u16..=222,
531 ) {
532 let buf = x10_sequence_bytes(encoded, x, y);
533 let msg = parse_mouse_event_sequence(&buf).unwrap();
534 let expected = expected_x10_mouse(encoded, x, y);
535 prop_assert_eq!(msg, expected);
536 }
537 }
538}