1use super::{BufferId, BufferMetadata, Editor};
27use crate::services::terminal::TerminalId;
28use crate::state::EditorState;
29use rust_i18n::t;
30
31impl Editor {
32 pub fn open_terminal(&mut self) {
34 let (cols, rows) = self.get_terminal_dimensions();
36
37 if let Some(ref bridge) = self.async_bridge {
39 self.terminal_manager.set_async_bridge(bridge.clone());
40 }
41
42 let terminal_root = self.dir_context.terminal_dir_for(&self.working_dir);
44 let _ = self.filesystem.create_dir_all(&terminal_root);
45 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
47 let log_path =
48 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
49 let backing_path =
50 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
51 self.terminal_backing_files
53 .insert(predicted_terminal_id, backing_path);
54
55 let backing_path_for_spawn = self
57 .terminal_backing_files
58 .get(&predicted_terminal_id)
59 .cloned();
60 match self.terminal_manager.spawn(
61 cols,
62 rows,
63 Some(self.working_dir.clone()),
64 Some(log_path.clone()),
65 backing_path_for_spawn,
66 ) {
67 Ok(terminal_id) => {
68 let actual_log_path = log_path.clone();
70 self.terminal_log_files
71 .insert(terminal_id, actual_log_path.clone());
72 if terminal_id != predicted_terminal_id {
74 self.terminal_backing_files.remove(&predicted_terminal_id);
75 let backing_path =
76 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
77 self.terminal_backing_files
78 .insert(terminal_id, backing_path);
79 }
80
81 let buffer_id = self.create_terminal_buffer_attached(
83 terminal_id,
84 self.split_manager.active_split(),
85 );
86
87 self.set_active_buffer(buffer_id);
89
90 self.terminal_mode = true;
92 self.key_context = crate::input::keybindings::KeyContext::Terminal;
93
94 self.resize_visible_terminals();
96
97 let exit_key = self
99 .keybindings
100 .find_keybinding_for_action(
101 "terminal_escape",
102 crate::input::keybindings::KeyContext::Terminal,
103 )
104 .unwrap_or_else(|| "Ctrl+Space".to_string());
105 self.set_status_message(
106 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
107 );
108 tracing::info!(
109 "Opened terminal {:?} with buffer {:?}",
110 terminal_id,
111 buffer_id
112 );
113 }
114 Err(e) => {
115 self.set_status_message(
116 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
117 );
118 tracing::error!("Failed to open terminal: {}", e);
119 }
120 }
121 }
122
123 fn create_terminal_buffer_attached(
125 &mut self,
126 terminal_id: TerminalId,
127 split_id: crate::model::event::SplitId,
128 ) -> BufferId {
129 let buffer_id = BufferId(self.next_buffer_id);
130 self.next_buffer_id += 1;
131
132 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
134
135 let backing_file = self
137 .terminal_backing_files
138 .get(&terminal_id)
139 .cloned()
140 .unwrap_or_else(|| {
141 let root = self.dir_context.terminal_dir_for(&self.working_dir);
142 let _ = self.filesystem.create_dir_all(&root);
143 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
144 });
145
146 if !self.filesystem.exists(&backing_file) {
149 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
150 tracing::warn!("Failed to create terminal backing file: {}", e);
151 }
152 }
153
154 self.terminal_backing_files
156 .insert(terminal_id, backing_file.clone());
157
158 let mut state = EditorState::new(
160 self.terminal_width,
161 self.terminal_height,
162 large_file_threshold,
163 std::sync::Arc::clone(&self.filesystem),
164 );
165 state.buffer.set_file_path(backing_file.clone());
166 state.margins.set_line_numbers(false);
168 self.buffers.insert(buffer_id, state);
169
170 let metadata = BufferMetadata::virtual_buffer(
173 format!("*Terminal {}*", terminal_id.0),
174 "terminal".into(),
175 false,
176 );
177 self.buffer_metadata.insert(buffer_id, metadata);
178
179 self.terminal_buffers.insert(buffer_id, terminal_id);
181
182 self.event_logs
184 .insert(buffer_id, crate::model::event::EventLog::new());
185
186 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
188 view_state.open_buffers.push(buffer_id);
189 view_state.viewport.line_wrap_enabled = false;
191 }
192
193 buffer_id
194 }
195
196 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
198 let buffer_id = BufferId(self.next_buffer_id);
199 self.next_buffer_id += 1;
200
201 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
203
204 let backing_file = self
205 .terminal_backing_files
206 .get(&terminal_id)
207 .cloned()
208 .unwrap_or_else(|| {
209 let root = self.dir_context.terminal_dir_for(&self.working_dir);
210 let _ = self.filesystem.create_dir_all(&root);
211 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
212 });
213
214 if !self.filesystem.exists(&backing_file) {
216 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
217 tracing::warn!("Failed to create terminal backing file: {}", e);
218 }
219 }
220
221 let mut state = EditorState::new(
223 self.terminal_width,
224 self.terminal_height,
225 large_file_threshold,
226 std::sync::Arc::clone(&self.filesystem),
227 );
228 state.buffer.set_file_path(backing_file.clone());
229 state.margins.set_line_numbers(false);
230 self.buffers.insert(buffer_id, state);
231
232 let metadata = BufferMetadata::virtual_buffer(
233 format!("*Terminal {}*", terminal_id.0),
234 "terminal".into(),
235 false,
236 );
237 self.buffer_metadata.insert(buffer_id, metadata);
238 self.terminal_buffers.insert(buffer_id, terminal_id);
239 self.event_logs
240 .insert(buffer_id, crate::model::event::EventLog::new());
241
242 buffer_id
243 }
244
245 pub fn close_terminal(&mut self) {
247 let buffer_id = self.active_buffer();
248
249 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
250 self.terminal_manager.close(terminal_id);
252 self.terminal_buffers.remove(&buffer_id);
253
254 let backing_file = self.terminal_backing_files.remove(&terminal_id);
256 if let Some(ref path) = backing_file {
257 let _ = self.filesystem.remove_file(path);
258 }
259 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
261 if backing_file.as_ref() != Some(&log_file) {
262 let _ = self.filesystem.remove_file(&log_file);
263 }
264 }
265
266 self.terminal_mode = false;
268 self.key_context = crate::input::keybindings::KeyContext::Normal;
269
270 let _ = self.close_buffer(buffer_id);
272
273 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
274 } else {
275 self.set_status_message(t!("status.not_viewing_terminal").to_string());
276 }
277 }
278
279 pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
281 self.terminal_buffers.contains_key(&buffer_id)
282 }
283
284 pub fn get_terminal_id(&self, buffer_id: BufferId) -> Option<TerminalId> {
286 self.terminal_buffers.get(&buffer_id).copied()
287 }
288
289 pub fn get_active_terminal_state(
291 &self,
292 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
293 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
294 let handle = self.terminal_manager.get(*terminal_id)?;
295 handle.state.lock().ok()
296 }
297
298 pub fn send_terminal_input(&mut self, data: &[u8]) {
300 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
301 if let Some(handle) = self.terminal_manager.get(terminal_id) {
302 handle.write(data);
303 }
304 }
305 }
306
307 pub fn send_terminal_key(
309 &mut self,
310 code: crossterm::event::KeyCode,
311 modifiers: crossterm::event::KeyModifiers,
312 ) {
313 if let Some(bytes) = crate::services::terminal::pty::key_to_pty_bytes(code, modifiers) {
314 self.send_terminal_input(&bytes);
315 }
316 }
317
318 pub fn send_terminal_mouse(
320 &mut self,
321 col: u16,
322 row: u16,
323 kind: crate::input::handler::TerminalMouseEventKind,
324 modifiers: crossterm::event::KeyModifiers,
325 ) {
326 use crate::input::handler::TerminalMouseEventKind;
327
328 let use_sgr = self
330 .get_active_terminal_state()
331 .map(|s| s.uses_sgr_mouse())
332 .unwrap_or(true); let uses_alt_scroll = self
336 .get_active_terminal_state()
337 .map(|s| s.uses_alternate_scroll())
338 .unwrap_or(false);
339
340 if uses_alt_scroll {
341 match kind {
342 TerminalMouseEventKind::ScrollUp => {
343 for _ in 0..3 {
345 self.send_terminal_input(b"\x1b[A");
346 }
347 return;
348 }
349 TerminalMouseEventKind::ScrollDown => {
350 for _ in 0..3 {
352 self.send_terminal_input(b"\x1b[B");
353 }
354 return;
355 }
356 _ => {}
357 }
358 }
359
360 let bytes = if use_sgr {
362 encode_sgr_mouse(col, row, kind, modifiers)
363 } else {
364 encode_x10_mouse(col, row, kind, modifiers)
365 };
366
367 if let Some(bytes) = bytes {
368 self.send_terminal_input(&bytes);
369 }
370 }
371
372 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
375 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
376 if let Some(handle) = self.terminal_manager.get(terminal_id) {
377 if let Ok(state) = handle.state.lock() {
378 return state.is_alternate_screen();
379 }
380 }
381 }
382 false
383 }
384
385 fn get_terminal_dimensions(&self) -> (u16, u16) {
387 let cols = self.terminal_width.saturating_sub(2).max(40);
390 let rows = self.terminal_height.saturating_sub(4).max(10);
391 (cols, rows)
392 }
393
394 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
396 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
397 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
398 handle.resize(cols, rows);
399 }
400 }
401 }
402
403 pub fn resize_visible_terminals(&mut self) {
406 let file_explorer_width = if self.file_explorer_visible {
408 (self.terminal_width as f32 * self.file_explorer_width_percent) as u16
409 } else {
410 0
411 };
412 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
413 let editor_area = ratatui::layout::Rect::new(
414 file_explorer_width,
415 1, editor_width,
417 self.terminal_height.saturating_sub(2), );
419
420 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
422
423 for (_split_id, buffer_id, split_area) in visible_buffers {
425 if self.terminal_buffers.contains_key(&buffer_id) {
426 let content_height = split_area.height.saturating_sub(2);
429 let content_width = split_area.width.saturating_sub(2);
430
431 if content_width > 0 && content_height > 0 {
432 self.resize_terminal(buffer_id, content_width, content_height);
433 }
434 }
435 }
436 }
437
438 pub fn handle_terminal_key(
440 &mut self,
441 code: crossterm::event::KeyCode,
442 modifiers: crossterm::event::KeyModifiers,
443 ) -> bool {
444 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
447 match code {
448 crossterm::event::KeyCode::Char(' ')
449 | crossterm::event::KeyCode::Char(']')
450 | crossterm::event::KeyCode::Char('`') => {
451 self.terminal_mode = false;
453 self.key_context = crate::input::keybindings::KeyContext::Normal;
454 self.sync_terminal_to_buffer(self.active_buffer());
455 self.set_status_message(
456 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
457 );
458 return true;
459 }
460 _ => {}
461 }
462 }
463
464 self.send_terminal_key(code, modifiers);
466 true
467 }
468
469 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
478 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
479 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
481 Some(path) => path.clone(),
482 None => return,
483 };
484
485 if let Some(handle) = self.terminal_manager.get(terminal_id) {
488 if let Ok(mut state) = handle.state.lock() {
489 if let Ok(metadata) = self.filesystem.metadata(&backing_file) {
492 state.set_backing_file_history_end(metadata.size);
493 }
494
495 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_file) {
497 use std::io::BufWriter;
498 let mut writer = BufWriter::new(&mut *file);
499 if let Err(e) = state.append_visible_screen(&mut writer) {
500 tracing::error!(
501 "Failed to append visible screen to backing file: {}",
502 e
503 );
504 }
505 }
506 }
507 }
508
509 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
511 if let Ok(new_state) = EditorState::from_file_with_languages(
512 &backing_file,
513 self.terminal_width,
514 self.terminal_height,
515 large_file_threshold,
516 &self.grammar_registry,
517 &self.config.languages,
518 std::sync::Arc::clone(&self.filesystem),
519 ) {
520 if let Some(state) = self.buffers.get_mut(&buffer_id) {
522 *state = new_state;
523 state.primary_cursor_mut().position = state.buffer.total_bytes();
525 state.buffer.set_modified(false);
527 }
528 }
529
530 if let Some(state) = self.buffers.get_mut(&buffer_id) {
532 state.editing_disabled = true;
533 state.margins.set_line_numbers(false);
534 }
535
536 if let Some(view_state) = self
539 .split_view_states
540 .get_mut(&self.split_manager.active_split())
541 {
542 view_state.viewport.line_wrap_enabled = false;
543
544 view_state.viewport.clear_skip_ensure_visible();
548
549 if let Some(state) = self.buffers.get_mut(&buffer_id) {
551 let cursor = *state.cursors.primary();
552 view_state
553 .viewport
554 .ensure_visible(&mut state.buffer, &cursor);
555 }
556 }
557 }
558 }
559
560 pub fn enter_terminal_mode(&mut self) {
566 if self.is_terminal_buffer(self.active_buffer()) {
567 self.terminal_mode = true;
568 self.key_context = crate::input::keybindings::KeyContext::Terminal;
569
570 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
572 state.editing_disabled = false;
573 state.margins.set_line_numbers(false);
574 }
575 if let Some(view_state) = self
576 .split_view_states
577 .get_mut(&self.split_manager.active_split())
578 {
579 view_state.viewport.line_wrap_enabled = false;
580 }
581
582 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
584 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
586 if let Some(handle) = self.terminal_manager.get(terminal_id) {
587 if let Ok(state) = handle.state.lock() {
588 let truncate_pos = state.backing_file_history_end();
589 if let Err(e) =
592 self.filesystem.set_file_length(backing_path, truncate_pos)
593 {
594 tracing::warn!("Failed to truncate terminal backing file: {}", e);
595 }
596 }
597 }
598 }
599
600 if let Some(handle) = self.terminal_manager.get(terminal_id) {
602 if let Ok(mut state) = handle.state.lock() {
603 state.scroll_to_bottom();
604 }
605 }
606 }
607
608 self.resize_visible_terminals();
610
611 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
612 }
613 }
614
615 pub fn get_terminal_content(
617 &self,
618 buffer_id: BufferId,
619 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
620 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
621 let handle = self.terminal_manager.get(*terminal_id)?;
622 let state = handle.state.lock().ok()?;
623
624 let (_, rows) = state.size();
625 let mut content = Vec::with_capacity(rows as usize);
626
627 for row in 0..rows {
628 content.push(state.get_line(row));
629 }
630
631 Some(content)
632 }
633}
634
635impl Editor {
636 pub fn is_terminal_mode(&self) -> bool {
638 self.terminal_mode
639 }
640
641 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
643 self.terminal_mode_resume.contains(&buffer_id)
644 }
645
646 pub fn is_keyboard_capture(&self) -> bool {
648 self.keyboard_capture
649 }
650
651 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
653 self.config.terminal.jump_to_end_on_output = value;
654 }
655
656 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
658 &self.terminal_manager
659 }
660
661 pub fn terminal_backing_files(
663 &self,
664 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
665 &self.terminal_backing_files
666 }
667
668 pub fn active_buffer_id(&self) -> BufferId {
670 self.active_buffer()
671 }
672
673 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
675 self.buffers
676 .get(&buffer_id)
677 .and_then(|state| state.buffer.to_string())
678 }
679
680 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
682 self.buffers
683 .get(&buffer_id)
684 .map(|state| state.primary_cursor().position)
685 }
686
687 pub fn render_terminal_splits(
693 &self,
694 frame: &mut ratatui::Frame,
695 split_areas: &[(
696 crate::model::event::SplitId,
697 BufferId,
698 ratatui::layout::Rect,
699 ratatui::layout::Rect,
700 usize,
701 usize,
702 )],
703 ) {
704 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
705 split_areas
706 {
707 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
709 let is_active = *buffer_id == self.active_buffer();
713 if is_active && !self.terminal_mode {
714 continue;
716 }
717 if let Some(handle) = self.terminal_manager.get(terminal_id) {
719 if let Ok(state) = handle.state.lock() {
720 let cursor_pos = state.cursor_position();
721 let cursor_visible =
723 state.cursor_visible() && is_active && self.terminal_mode;
724 let (_, rows) = state.size();
725
726 let mut content = Vec::with_capacity(rows as usize);
728 for row in 0..rows {
729 content.push(state.get_line(row));
730 }
731
732 frame.render_widget(ratatui::widgets::Clear, *content_rect);
734
735 render::render_terminal_content(
737 &content,
738 cursor_pos,
739 cursor_visible,
740 *content_rect,
741 frame.buffer_mut(),
742 self.theme.terminal_fg,
743 self.theme.terminal_bg,
744 );
745 }
746 }
747 }
748 }
749 }
750}
751
752pub mod render {
754 use crate::services::terminal::TerminalCell;
755 use ratatui::buffer::Buffer;
756 use ratatui::layout::Rect;
757 use ratatui::style::{Color, Modifier, Style};
758
759 pub fn render_terminal_content(
761 content: &[Vec<TerminalCell>],
762 cursor_pos: (u16, u16),
763 cursor_visible: bool,
764 area: Rect,
765 buf: &mut Buffer,
766 default_fg: Color,
767 default_bg: Color,
768 ) {
769 for (row_idx, row) in content.iter().enumerate() {
770 if row_idx as u16 >= area.height {
771 break;
772 }
773
774 let y = area.y + row_idx as u16;
775
776 for (col_idx, cell) in row.iter().enumerate() {
777 if col_idx as u16 >= area.width {
778 break;
779 }
780
781 let x = area.x + col_idx as u16;
782
783 let mut style = Style::default().fg(default_fg).bg(default_bg);
785
786 if let Some((r, g, b)) = cell.fg {
788 style = style.fg(Color::Rgb(r, g, b));
789 }
790
791 if let Some((r, g, b)) = cell.bg {
792 style = style.bg(Color::Rgb(r, g, b));
793 }
794
795 if cell.bold {
797 style = style.add_modifier(Modifier::BOLD);
798 }
799 if cell.italic {
800 style = style.add_modifier(Modifier::ITALIC);
801 }
802 if cell.underline {
803 style = style.add_modifier(Modifier::UNDERLINED);
804 }
805 if cell.inverse {
806 style = style.add_modifier(Modifier::REVERSED);
807 }
808
809 if cursor_visible
811 && row_idx as u16 == cursor_pos.1
812 && col_idx as u16 == cursor_pos.0
813 {
814 style = style.add_modifier(Modifier::REVERSED);
815 }
816
817 buf.set_string(x, y, cell.c.to_string(), style);
818 }
819 }
820 }
821}
822
823fn encode_sgr_mouse(
826 col: u16,
827 row: u16,
828 kind: crate::input::handler::TerminalMouseEventKind,
829 modifiers: crossterm::event::KeyModifiers,
830) -> Option<Vec<u8>> {
831 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
832
833 let cx = col + 1;
835 let cy = row + 1;
836
837 let (button_code, is_release) = match kind {
839 TerminalMouseEventKind::Down(btn) => {
840 let code = match btn {
841 TerminalMouseButton::Left => 0,
842 TerminalMouseButton::Middle => 1,
843 TerminalMouseButton::Right => 2,
844 };
845 (code, false)
846 }
847 TerminalMouseEventKind::Up(btn) => {
848 let code = match btn {
849 TerminalMouseButton::Left => 0,
850 TerminalMouseButton::Middle => 1,
851 TerminalMouseButton::Right => 2,
852 };
853 (code, true)
854 }
855 TerminalMouseEventKind::Drag(btn) => {
856 let code = match btn {
857 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
861 (code, false)
862 }
863 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
865 TerminalMouseEventKind::ScrollDown => (65, false),
866 };
867
868 let mut cb = button_code;
870 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
871 cb += 4;
872 }
873 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
874 cb += 8;
875 }
876 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
877 cb += 16;
878 }
879
880 let terminator = if is_release { 'm' } else { 'M' };
882 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
883}
884
885fn encode_x10_mouse(
888 col: u16,
889 row: u16,
890 kind: crate::input::handler::TerminalMouseEventKind,
891 modifiers: crossterm::event::KeyModifiers,
892) -> Option<Vec<u8>> {
893 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
894
895 let cx = (col.min(222) + 1 + 32) as u8;
898 let cy = (row.min(222) + 1 + 32) as u8;
899
900 let button_code: u8 = match kind {
902 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
903 TerminalMouseButton::Left => 0,
904 TerminalMouseButton::Middle => 1,
905 TerminalMouseButton::Right => 2,
906 },
907 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
909 TerminalMouseEventKind::ScrollUp => 64,
910 TerminalMouseEventKind::ScrollDown => 65,
911 };
912
913 let mut cb = button_code;
915 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
916 cb += 32; }
918 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
919 cb += 4;
920 }
921 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
922 cb += 8;
923 }
924 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
925 cb += 16;
926 }
927
928 let cb = cb + 32;
930
931 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
932}