1use std::cmp::{self, min};
2use std::iter::repeat;
3
4use alacritty_terminal::grid::Dimensions;
5/// Most of the code, and specifically the constants, in this are copied from Alacritty,
6/// with modifications for our circumstances
7use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side};
8use alacritty_terminal::term::TermMode;
9use gpui::{Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent, px};
10
11use crate::TerminalBounds;
12
13enum MouseFormat {
14 Sgr,
15 Normal(bool),
16}
17
18impl MouseFormat {
19 fn from_mode(mode: TermMode) -> Self {
20 if mode.contains(TermMode::SGR_MOUSE) {
21 MouseFormat::Sgr
22 } else if mode.contains(TermMode::UTF8_MOUSE) {
23 MouseFormat::Normal(true)
24 } else {
25 MouseFormat::Normal(false)
26 }
27 }
28}
29
30#[derive(Debug)]
31enum AlacMouseButton {
32 LeftButton = 0,
33 MiddleButton = 1,
34 RightButton = 2,
35 LeftMove = 32,
36 MiddleMove = 33,
37 RightMove = 34,
38 NoneMove = 35,
39 ScrollUp = 64,
40 ScrollDown = 65,
41 Other = 99,
42}
43
44impl AlacMouseButton {
45 fn from_move_button(e: Option<MouseButton>) -> Self {
46 match e {
47 Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove,
48 Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove,
49 Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove,
50 Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other,
51 None => AlacMouseButton::NoneMove,
52 }
53 }
54
55 fn from_button(e: MouseButton) -> Self {
56 match e {
57 gpui::MouseButton::Left => AlacMouseButton::LeftButton,
58 gpui::MouseButton::Right => AlacMouseButton::MiddleButton,
59 gpui::MouseButton::Middle => AlacMouseButton::RightButton,
60 gpui::MouseButton::Navigate(_) => AlacMouseButton::Other,
61 }
62 }
63
64 fn from_scroll(e: &ScrollWheelEvent) -> Self {
65 let is_positive = match e.delta {
66 gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.),
67 gpui::ScrollDelta::Lines(lines) => lines.y > 0.,
68 };
69
70 if is_positive {
71 AlacMouseButton::ScrollUp
72 } else {
73 AlacMouseButton::ScrollDown
74 }
75 }
76
77 fn is_other(&self) -> bool {
78 matches!(self, AlacMouseButton::Other)
79 }
80}
81
82pub fn scroll_report(
83 point: AlacPoint,
84 scroll_lines: i32,
85 e: &ScrollWheelEvent,
86 mode: TermMode,
87) -> Option<impl Iterator<Item = Vec<u8>>> {
88 if mode.intersects(TermMode::MOUSE_MODE) {
89 mouse_report(
90 point,
91 AlacMouseButton::from_scroll(e),
92 true,
93 e.modifiers,
94 MouseFormat::from_mode(mode),
95 )
96 .map(|report| repeat(report).take(scroll_lines.unsigned_abs() as usize))
97 } else {
98 None
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use gpui::{ScrollDelta, TouchPhase, point};
106
107 #[test]
108 fn scroll_report_repeats_for_negative_scroll_lines() {
109 let grid_point = AlacPoint::new(GridLine(0), GridCol(0));
110
111 let scroll_event = ScrollWheelEvent {
112 delta: ScrollDelta::Lines(point(0., -1.)),
113 touch_phase: TouchPhase::Moved,
114 ..Default::default()
115 };
116
117 let mode = TermMode::MOUSE_MODE;
118 let reports: Vec<Vec<u8>> = scroll_report(grid_point, -3, &scroll_event, mode)
119 .expect("mouse mode should produce a scroll report")
120 .collect();
121
122 assert_eq!(reports.len(), 3);
123 }
124
125 #[test]
126 fn scroll_report_repeats_for_positive_scroll_lines() {
127 let grid_point = AlacPoint::new(GridLine(0), GridCol(0));
128
129 let scroll_event = ScrollWheelEvent {
130 delta: ScrollDelta::Lines(point(0., 1.)),
131 touch_phase: TouchPhase::Moved,
132 ..Default::default()
133 };
134
135 let mode = TermMode::MOUSE_MODE;
136 let reports: Vec<Vec<u8>> = scroll_report(grid_point, 3, &scroll_event, mode)
137 .expect("mouse mode should produce a scroll report")
138 .collect();
139
140 assert_eq!(reports.len(), 3);
141 }
142}
143
144pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
145 let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
146
147 let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3);
148 for _ in 0..scroll_lines.abs() {
149 content.push(0x1b);
150 content.push(b'O');
151 content.push(cmd);
152 }
153 content
154}
155
156pub fn mouse_button_report(
157 point: AlacPoint,
158 button: gpui::MouseButton,
159 modifiers: Modifiers,
160 pressed: bool,
161 mode: TermMode,
162) -> Option<Vec<u8>> {
163 let button = AlacMouseButton::from_button(button);
164 if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
165 mouse_report(
166 point,
167 button,
168 pressed,
169 modifiers,
170 MouseFormat::from_mode(mode),
171 )
172 } else {
173 None
174 }
175}
176
177pub fn mouse_moved_report(
178 point: AlacPoint,
179 button: Option<MouseButton>,
180 modifiers: Modifiers,
181 mode: TermMode,
182) -> Option<Vec<u8>> {
183 let button = AlacMouseButton::from_move_button(button);
184
185 if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
186 //Only drags are reported in drag mode, so block NoneMove.
187 if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) {
188 None
189 } else {
190 mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode))
191 }
192 } else {
193 None
194 }
195}
196
197pub fn grid_point(
198 pos: Point<Pixels>,
199 cur_size: TerminalBounds,
200 display_offset: usize,
201) -> AlacPoint {
202 grid_point_and_side(pos, cur_size, display_offset).0
203}
204
205pub fn grid_point_and_side(
206 pos: Point<Pixels>,
207 cur_size: TerminalBounds,
208 display_offset: usize,
209) -> (AlacPoint, Side) {
210 let mut col = GridCol((pos.x / cur_size.cell_width) as usize);
211 let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width;
212 let half_cell_width = cur_size.cell_width / 2.0;
213 let mut side = if cell_x > half_cell_width {
214 Side::Right
215 } else {
216 Side::Left
217 };
218
219 if col > cur_size.last_column() {
220 col = cur_size.last_column();
221 side = Side::Right;
222 }
223 let col = min(col, cur_size.last_column());
224 let mut line = (pos.y / cur_size.line_height) as i32;
225 if line > cur_size.bottommost_line() {
226 line = cur_size.bottommost_line().0;
227 side = Side::Right;
228 } else if line < 0 {
229 side = Side::Left;
230 }
231
232 (
233 AlacPoint::new(GridLine(line - display_offset as i32), col),
234 side,
235 )
236}
237
238///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
239fn mouse_report(
240 point: AlacPoint,
241 button: AlacMouseButton,
242 pressed: bool,
243 modifiers: Modifiers,
244 format: MouseFormat,
245) -> Option<Vec<u8>> {
246 if point.line < 0 {
247 return None;
248 }
249
250 let mut mods = 0;
251 if modifiers.shift {
252 mods += 4;
253 }
254 if modifiers.alt {
255 mods += 8;
256 }
257 if modifiers.control {
258 mods += 16;
259 }
260
261 match format {
262 MouseFormat::Sgr => {
263 Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
264 }
265 MouseFormat::Normal(utf8) => {
266 if pressed {
267 normal_mouse_report(point, button as u8 + mods, utf8)
268 } else {
269 normal_mouse_report(point, 3 + mods, utf8)
270 }
271 }
272 }
273}
274
275fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option<Vec<u8>> {
276 let AlacPoint { line, column } = point;
277 let max_point = if utf8 { 2015 } else { 223 };
278
279 if line >= max_point || column >= max_point {
280 return None;
281 }
282
283 let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
284
285 let mouse_pos_encode = |pos: usize| -> Vec<u8> {
286 let pos = 32 + 1 + pos;
287 let first = 0xC0 + pos / 64;
288 let second = 0x80 + (pos & 63);
289 vec![first as u8, second as u8]
290 };
291
292 if utf8 && column >= 95 {
293 msg.append(&mut mouse_pos_encode(column.0));
294 } else {
295 msg.push(32 + 1 + column.0 as u8);
296 }
297
298 if utf8 && line >= 95 {
299 msg.append(&mut mouse_pos_encode(line.0 as usize));
300 } else {
301 msg.push(32 + 1 + line.0 as u8);
302 }
303
304 Some(msg)
305}
306
307fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String {
308 let c = if pressed { 'M' } else { 'm' };
309
310 let msg = format!(
311 "\x1b[<{};{};{}{}",
312 button,
313 point.column + 1,
314 point.line + 1,
315 c
316 );
317
318 msg
319}