1use std::cmp::{self, max, 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::{px, Modifiers, MouseButton, MouseMoveEvent, Pixels, Point, ScrollWheelEvent};
10
11use crate::TerminalSize;
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(e: &MouseMoveEvent) -> Self {
46 match e.pressed_button {
47 Some(b) => match b {
48 gpui::MouseButton::Left => AlacMouseButton::LeftMove,
49 gpui::MouseButton::Middle => AlacMouseButton::MiddleMove,
50 gpui::MouseButton::Right => AlacMouseButton::RightMove,
51 gpui::MouseButton::Navigate(_) => AlacMouseButton::Other,
52 },
53 None => AlacMouseButton::NoneMove,
54 }
55 }
56
57 fn from_button(e: MouseButton) -> Self {
58 match e {
59 gpui::MouseButton::Left => AlacMouseButton::LeftButton,
60 gpui::MouseButton::Right => AlacMouseButton::MiddleButton,
61 gpui::MouseButton::Middle => AlacMouseButton::RightButton,
62 gpui::MouseButton::Navigate(_) => AlacMouseButton::Other,
63 }
64 }
65
66 fn from_scroll(e: &ScrollWheelEvent) -> Self {
67 let is_positive = match e.delta {
68 gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.),
69 gpui::ScrollDelta::Lines(lines) => lines.y > 0.,
70 };
71
72 if is_positive {
73 AlacMouseButton::ScrollUp
74 } else {
75 AlacMouseButton::ScrollDown
76 }
77 }
78
79 fn is_other(&self) -> bool {
80 matches!(self, AlacMouseButton::Other)
81 }
82}
83
84pub fn scroll_report(
85 point: AlacPoint,
86 scroll_lines: i32,
87 e: &ScrollWheelEvent,
88 mode: TermMode,
89) -> Option<impl Iterator<Item = Vec<u8>>> {
90 if mode.intersects(TermMode::MOUSE_MODE) {
91 mouse_report(
92 point,
93 AlacMouseButton::from_scroll(e),
94 true,
95 e.modifiers,
96 MouseFormat::from_mode(mode),
97 )
98 .map(|report| repeat(report).take(max(scroll_lines, 1) as usize))
99 } else {
100 None
101 }
102}
103
104pub fn alt_scroll(scroll_lines: i32) -> Vec<u8> {
105 let cmd = if scroll_lines > 0 { b'A' } else { b'B' };
106
107 let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3);
108 for _ in 0..scroll_lines.abs() {
109 content.push(0x1b);
110 content.push(b'O');
111 content.push(cmd);
112 }
113 content
114}
115
116pub fn mouse_button_report(
117 point: AlacPoint,
118 button: gpui::MouseButton,
119 modifiers: Modifiers,
120 pressed: bool,
121 mode: TermMode,
122) -> Option<Vec<u8>> {
123 let button = AlacMouseButton::from_button(button);
124 if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) {
125 mouse_report(
126 point,
127 button,
128 pressed,
129 modifiers,
130 MouseFormat::from_mode(mode),
131 )
132 } else {
133 None
134 }
135}
136
137pub fn mouse_moved_report(point: AlacPoint, e: &MouseMoveEvent, mode: TermMode) -> Option<Vec<u8>> {
138 let button = AlacMouseButton::from_move(e);
139
140 if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) {
141 //Only drags are reported in drag mode, so block NoneMove.
142 if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) {
143 None
144 } else {
145 mouse_report(
146 point,
147 button,
148 true,
149 e.modifiers,
150 MouseFormat::from_mode(mode),
151 )
152 }
153 } else {
154 None
155 }
156}
157
158pub fn grid_point(pos: Point<Pixels>, cur_size: TerminalSize, display_offset: usize) -> AlacPoint {
159 grid_point_and_side(pos, cur_size, display_offset).0
160}
161
162pub fn grid_point_and_side(
163 pos: Point<Pixels>,
164 cur_size: TerminalSize,
165 display_offset: usize,
166) -> (AlacPoint, Side) {
167 let mut col = GridCol((pos.x / cur_size.cell_width) as usize);
168 let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width;
169 let half_cell_width = cur_size.cell_width / 2.0;
170 let mut side = if cell_x > half_cell_width {
171 Side::Right
172 } else {
173 Side::Left
174 };
175
176 if col > cur_size.last_column() {
177 col = cur_size.last_column();
178 side = Side::Right;
179 }
180 let col = min(col, cur_size.last_column());
181 let mut line = (pos.y / cur_size.line_height) as i32;
182 if line > cur_size.bottommost_line() {
183 line = cur_size.bottommost_line().0;
184 side = Side::Right;
185 } else if line < 0 {
186 side = Side::Left;
187 }
188
189 (
190 AlacPoint::new(GridLine(line - display_offset as i32), col),
191 side,
192 )
193}
194
195///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode
196fn mouse_report(
197 point: AlacPoint,
198 button: AlacMouseButton,
199 pressed: bool,
200 modifiers: Modifiers,
201 format: MouseFormat,
202) -> Option<Vec<u8>> {
203 if point.line < 0 {
204 return None;
205 }
206
207 let mut mods = 0;
208 if modifiers.shift {
209 mods += 4;
210 }
211 if modifiers.alt {
212 mods += 8;
213 }
214 if modifiers.control {
215 mods += 16;
216 }
217
218 match format {
219 MouseFormat::Sgr => {
220 Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes())
221 }
222 MouseFormat::Normal(utf8) => {
223 if pressed {
224 normal_mouse_report(point, button as u8 + mods, utf8)
225 } else {
226 normal_mouse_report(point, 3 + mods, utf8)
227 }
228 }
229 }
230}
231
232fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option<Vec<u8>> {
233 let AlacPoint { line, column } = point;
234 let max_point = if utf8 { 2015 } else { 223 };
235
236 if line >= max_point || column >= max_point {
237 return None;
238 }
239
240 let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
241
242 let mouse_pos_encode = |pos: usize| -> Vec<u8> {
243 let pos = 32 + 1 + pos;
244 let first = 0xC0 + pos / 64;
245 let second = 0x80 + (pos & 63);
246 vec![first as u8, second as u8]
247 };
248
249 if utf8 && column >= 95 {
250 msg.append(&mut mouse_pos_encode(column.0));
251 } else {
252 msg.push(32 + 1 + column.0 as u8);
253 }
254
255 if utf8 && line >= 95 {
256 msg.append(&mut mouse_pos_encode(line.0 as usize));
257 } else {
258 msg.push(32 + 1 + line.0 as u8);
259 }
260
261 Some(msg)
262}
263
264fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String {
265 let c = if pressed { 'M' } else { 'm' };
266
267 let msg = format!(
268 "\x1b[<{};{};{}{}",
269 button,
270 point.column + 1,
271 point.line + 1,
272 c
273 );
274
275 msg
276}