Rust · 6839 bytes Raw Blame History
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