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 #[test]
420 fn test_from_crossterm_mouse_drag_maps_to_motion_with_button() {
421 use crossterm::event::{KeyModifiers, MouseButton as CtButton, MouseEvent, MouseEventKind};
422
423 let event = MouseEvent {
424 kind: MouseEventKind::Drag(CtButton::Left),
425 column: 12,
426 row: 34,
427 modifiers: KeyModifiers::empty(),
428 };
429
430 let msg = from_crossterm_mouse(event);
431 assert_eq!(msg.x, 12);
432 assert_eq!(msg.y, 34);
433 assert_eq!(msg.action, MouseAction::Motion);
434 assert_eq!(msg.button, MouseButton::Left);
435 assert!(!msg.shift);
436 assert!(!msg.alt);
437 assert!(!msg.ctrl);
438 }
439
440 #[test]
441 fn test_from_crossterm_mouse_moved_maps_to_motion_without_button() {
442 use crossterm::event::{KeyModifiers, MouseEvent, MouseEventKind};
443
444 let event = MouseEvent {
445 kind: MouseEventKind::Moved,
446 column: 1,
447 row: 2,
448 modifiers: KeyModifiers::empty(),
449 };
450
451 let msg = from_crossterm_mouse(event);
452 assert_eq!(msg.action, MouseAction::Motion);
453 assert_eq!(msg.button, MouseButton::None);
454 }
455
456 fn sgr_sequence_bytes(encoded: u16, x: u16, y: u16, release: bool) -> Vec<u8> {
457 let suffix = if release { 'm' } else { 'M' };
458 format!("\x1b[<{};{};{}{}", encoded, x, y, suffix).into_bytes()
459 }
460
461 fn expected_sgr_mouse(encoded: u16, x: u16, y: u16, release: bool) -> MouseMsg {
462 let mut parsed = parse_mouse_button(encoded, true);
463 if release && parsed.action != MouseAction::Motion && !is_wheel_button(parsed.button) {
464 parsed.action = MouseAction::Release;
465 }
466 MouseMsg {
467 x: x - 1,
468 y: y - 1,
469 shift: parsed.shift,
470 alt: parsed.alt,
471 ctrl: parsed.ctrl,
472 action: parsed.action,
473 button: parsed.button,
474 }
475 }
476
477 fn x10_sequence_bytes(encoded: u8, x: u16, y: u16) -> [u8; 6] {
478 let x_byte = (x + 33) as u8;
479 let y_byte = (y + 33) as u8;
480 [0x1b, b'[', b'M', encoded, x_byte, y_byte]
481 }
482
483 fn expected_x10_mouse(encoded: u8, x: u16, y: u16) -> MouseMsg {
484 let parsed = parse_mouse_button(u16::from(encoded), false);
485 MouseMsg {
486 x,
487 y,
488 shift: parsed.shift,
489 alt: parsed.alt,
490 ctrl: parsed.ctrl,
491 action: parsed.action,
492 button: parsed.button,
493 }
494 }
495
496 #[test]
497 fn test_mouse_msg_display() {
498 let mouse = MouseMsg {
499 x: 10,
500 y: 20,
501 shift: false,
502 alt: false,
503 ctrl: false,
504 action: MouseAction::Press,
505 button: MouseButton::Left,
506 };
507 assert_eq!(mouse.to_string(), "left press");
508
509 let mouse = MouseMsg {
510 x: 10,
511 y: 20,
512 shift: false,
513 alt: false,
514 ctrl: true,
515 action: MouseAction::Press,
516 button: MouseButton::Left,
517 };
518 assert_eq!(mouse.to_string(), "ctrl+left press");
519 }
520
521 #[test]
522 fn test_mouse_is_wheel() {
523 let mouse = MouseMsg {
524 button: MouseButton::WheelUp,
525 ..Default::default()
526 };
527 assert!(mouse.is_wheel());
528
529 let mouse = MouseMsg {
530 button: MouseButton::Left,
531 ..Default::default()
532 };
533 assert!(!mouse.is_wheel());
534 }
535
536 #[test]
537 fn test_mouse_button_display() {
538 assert_eq!(MouseButton::Left.to_string(), "left");
539 assert_eq!(MouseButton::WheelUp.to_string(), "wheel up");
540 }
541
542 #[test]
543 fn test_mouse_action_display() {
544 assert_eq!(MouseAction::Press.to_string(), "press");
545 assert_eq!(MouseAction::Release.to_string(), "release");
546 assert_eq!(MouseAction::Motion.to_string(), "motion");
547 }
548
549 proptest! {
550 #[test]
551 fn prop_parse_sgr_mouse_roundtrip(
552 encoded in 0u16..=255,
553 x in 1u16..=2000,
554 y in 1u16..=2000,
555 release in any::<bool>(),
556 ) {
557 let buf = sgr_sequence_bytes(encoded, x, y, release);
558 let msg = parse_mouse_event_sequence(&buf).unwrap();
559 let expected = expected_sgr_mouse(encoded, x, y, release);
560 prop_assert_eq!(msg, expected);
561 }
562
563 #[test]
564 fn prop_parse_x10_mouse_roundtrip(
565 encoded in 32u8..=255,
566 x in 0u16..=222,
567 y in 0u16..=222,
568 ) {
569 let buf = x10_sequence_bytes(encoded, x, y);
570 let msg = parse_mouse_event_sequence(&buf).unwrap();
571 let expected = expected_x10_mouse(encoded, x, y);
572 prop_assert_eq!(msg, expected);
573 }
574 }
575}