| 1 | //! Mouse input handling for terminal |
| 2 | |
| 3 | use crate::terminal::{MouseEncoding, MouseMode}; |
| 4 | |
| 5 | /// Mouse button identifier |
| 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 7 | pub enum MouseButton { |
| 8 | Left, |
| 9 | Middle, |
| 10 | Right, |
| 11 | /// Scroll up |
| 12 | WheelUp, |
| 13 | /// Scroll down |
| 14 | WheelDown, |
| 15 | /// No button (motion only) |
| 16 | None, |
| 17 | } |
| 18 | |
| 19 | impl MouseButton { |
| 20 | /// Get the button code for mouse reporting |
| 21 | fn code(&self) -> u8 { |
| 22 | match self { |
| 23 | MouseButton::Left => 0, |
| 24 | MouseButton::Middle => 1, |
| 25 | MouseButton::Right => 2, |
| 26 | MouseButton::WheelUp => 64, |
| 27 | MouseButton::WheelDown => 65, |
| 28 | MouseButton::None => 3, // Release or motion |
| 29 | } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /// Mouse event type |
| 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 35 | pub enum MouseEvent { |
| 36 | Press(MouseButton), |
| 37 | Release(MouseButton), |
| 38 | Motion, |
| 39 | Drag(MouseButton), |
| 40 | } |
| 41 | |
| 42 | /// Mouse handler for generating terminal escape sequences |
| 43 | pub struct MouseHandler; |
| 44 | |
| 45 | impl MouseHandler { |
| 46 | /// Generate mouse escape sequence |
| 47 | /// |
| 48 | /// Returns None if mouse reporting is disabled for this event type |
| 49 | pub fn encode( |
| 50 | event: MouseEvent, |
| 51 | col: usize, |
| 52 | row: usize, |
| 53 | shift: bool, |
| 54 | alt: bool, |
| 55 | ctrl: bool, |
| 56 | mode: MouseMode, |
| 57 | encoding: MouseEncoding, |
| 58 | ) -> Option<Vec<u8>> { |
| 59 | // Check if this event should be reported |
| 60 | if !Self::should_report(event, mode) { |
| 61 | return None; |
| 62 | } |
| 63 | |
| 64 | // Calculate button code with modifiers |
| 65 | let (button, is_release) = match event { |
| 66 | MouseEvent::Press(btn) => (btn, false), |
| 67 | MouseEvent::Release(btn) => (btn, true), |
| 68 | MouseEvent::Motion => (MouseButton::None, false), |
| 69 | MouseEvent::Drag(btn) => (btn, false), |
| 70 | }; |
| 71 | |
| 72 | let mut code = button.code(); |
| 73 | |
| 74 | // Add modifier flags |
| 75 | if shift { |
| 76 | code |= 4; |
| 77 | } |
| 78 | if alt { |
| 79 | code |= 8; |
| 80 | } |
| 81 | if ctrl { |
| 82 | code |= 16; |
| 83 | } |
| 84 | |
| 85 | // Motion flag |
| 86 | if matches!(event, MouseEvent::Motion | MouseEvent::Drag(_)) { |
| 87 | code |= 32; |
| 88 | } |
| 89 | |
| 90 | // Convert to 1-indexed coordinates |
| 91 | let col = col.saturating_add(1); |
| 92 | let row = row.saturating_add(1); |
| 93 | |
| 94 | match encoding { |
| 95 | MouseEncoding::X10 => Self::encode_x10(code, col, row, is_release), |
| 96 | MouseEncoding::Utf8 => Self::encode_utf8(code, col, row, is_release), |
| 97 | MouseEncoding::Sgr => Self::encode_sgr(code, col, row, is_release), |
| 98 | MouseEncoding::Urxvt => Self::encode_urxvt(code, col, row, is_release), |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | /// Check if event should be reported based on mouse mode |
| 103 | fn should_report(event: MouseEvent, mode: MouseMode) -> bool { |
| 104 | match mode { |
| 105 | MouseMode::None => false, |
| 106 | MouseMode::X10 => matches!(event, MouseEvent::Press(_)), |
| 107 | MouseMode::Vt200 => matches!(event, MouseEvent::Press(_) | MouseEvent::Release(_)), |
| 108 | MouseMode::ButtonEvent => { |
| 109 | matches!(event, MouseEvent::Press(_) | MouseEvent::Release(_) | MouseEvent::Drag(_)) |
| 110 | } |
| 111 | MouseMode::AnyEvent => true, |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | /// X10 encoding: ESC [ M Cb Cx Cy |
| 116 | fn encode_x10(code: u8, col: usize, row: usize, is_release: bool) -> Option<Vec<u8>> { |
| 117 | // X10 mode doesn't report release |
| 118 | if is_release { |
| 119 | return None; |
| 120 | } |
| 121 | |
| 122 | // X10 encoding limited to 223 (+ 32 = 255) |
| 123 | if col > 223 || row > 223 { |
| 124 | return None; |
| 125 | } |
| 126 | |
| 127 | Some(vec![ |
| 128 | 0x1b, |
| 129 | b'[', |
| 130 | b'M', |
| 131 | code + 32, |
| 132 | (col as u8) + 32, |
| 133 | (row as u8) + 32, |
| 134 | ]) |
| 135 | } |
| 136 | |
| 137 | /// UTF-8 encoding: ESC [ M Cb Cx Cy (with UTF-8 for large values) |
| 138 | fn encode_utf8(code: u8, col: usize, row: usize, is_release: bool) -> Option<Vec<u8>> { |
| 139 | let mut result = vec![0x1b, b'[', b'M']; |
| 140 | |
| 141 | // Code byte |
| 142 | result.push(code + 32); |
| 143 | |
| 144 | // Column (UTF-8 encoded if > 127) |
| 145 | Self::push_utf8_coord(&mut result, col); |
| 146 | |
| 147 | // Row (UTF-8 encoded if > 127) |
| 148 | Self::push_utf8_coord(&mut result, row); |
| 149 | |
| 150 | // For release in UTF-8 mode, code 3 is used |
| 151 | if is_release { |
| 152 | result[3] = 3 + 32; |
| 153 | } |
| 154 | |
| 155 | Some(result) |
| 156 | } |
| 157 | |
| 158 | fn push_utf8_coord(result: &mut Vec<u8>, coord: usize) { |
| 159 | let val = (coord as u32) + 32; |
| 160 | if val < 128 { |
| 161 | result.push(val as u8); |
| 162 | } else { |
| 163 | // UTF-8 encode |
| 164 | let mut buf = [0u8; 4]; |
| 165 | let s = char::from_u32(val).unwrap_or(' ').encode_utf8(&mut buf); |
| 166 | result.extend_from_slice(s.as_bytes()); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | /// SGR encoding: ESC [ < Pb ; Px ; Py M/m |
| 171 | fn encode_sgr(code: u8, col: usize, row: usize, is_release: bool) -> Option<Vec<u8>> { |
| 172 | let terminator = if is_release { b'm' } else { b'M' }; |
| 173 | Some(format!("\x1b[<{};{};{}{}", code, col, row, terminator as char).into_bytes()) |
| 174 | } |
| 175 | |
| 176 | /// urxvt encoding: ESC [ Pb ; Px ; Py M |
| 177 | fn encode_urxvt(code: u8, col: usize, row: usize, is_release: bool) -> Option<Vec<u8>> { |
| 178 | // urxvt uses code 3 for release |
| 179 | let code = if is_release { 3 } else { code }; |
| 180 | Some(format!("\x1b[{};{};{}M", code + 32, col, row).into_bytes()) |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | #[cfg(test)] |
| 185 | mod tests { |
| 186 | use super::*; |
| 187 | |
| 188 | #[test] |
| 189 | fn test_sgr_press() { |
| 190 | let result = MouseHandler::encode( |
| 191 | MouseEvent::Press(MouseButton::Left), |
| 192 | 10, |
| 193 | 20, |
| 194 | false, |
| 195 | false, |
| 196 | false, |
| 197 | MouseMode::ButtonEvent, |
| 198 | MouseEncoding::Sgr, |
| 199 | ); |
| 200 | assert_eq!(result, Some(b"\x1b[<0;11;21M".to_vec())); |
| 201 | } |
| 202 | |
| 203 | #[test] |
| 204 | fn test_sgr_release() { |
| 205 | let result = MouseHandler::encode( |
| 206 | MouseEvent::Release(MouseButton::Left), |
| 207 | 10, |
| 208 | 20, |
| 209 | false, |
| 210 | false, |
| 211 | false, |
| 212 | MouseMode::ButtonEvent, |
| 213 | MouseEncoding::Sgr, |
| 214 | ); |
| 215 | assert_eq!(result, Some(b"\x1b[<0;11;21m".to_vec())); |
| 216 | } |
| 217 | |
| 218 | #[test] |
| 219 | fn test_x10_no_release() { |
| 220 | let result = MouseHandler::encode( |
| 221 | MouseEvent::Release(MouseButton::Left), |
| 222 | 10, |
| 223 | 20, |
| 224 | false, |
| 225 | false, |
| 226 | false, |
| 227 | MouseMode::X10, |
| 228 | MouseEncoding::X10, |
| 229 | ); |
| 230 | assert_eq!(result, None); |
| 231 | } |
| 232 | |
| 233 | #[test] |
| 234 | fn test_x10_press() { |
| 235 | let result = MouseHandler::encode( |
| 236 | MouseEvent::Press(MouseButton::Left), |
| 237 | 0, |
| 238 | 0, |
| 239 | false, |
| 240 | false, |
| 241 | false, |
| 242 | MouseMode::X10, |
| 243 | MouseEncoding::X10, |
| 244 | ); |
| 245 | assert_eq!(result, Some(vec![0x1b, b'[', b'M', 32, 33, 33])); |
| 246 | } |
| 247 | } |
| 248 |