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