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 if let Err(e) = self.filesystem.create_dir_all(&terminal_root) {
45 tracing::warn!("Failed to create terminal directory: {}", e);
46 }
47 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
49 let log_path =
50 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
51 let backing_path =
52 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
53 self.terminal_backing_files
55 .insert(predicted_terminal_id, backing_path);
56
57 let backing_path_for_spawn = self
59 .terminal_backing_files
60 .get(&predicted_terminal_id)
61 .cloned();
62 match self.terminal_manager.spawn(
63 cols,
64 rows,
65 Some(self.working_dir.clone()),
66 Some(log_path.clone()),
67 backing_path_for_spawn,
68 ) {
69 Ok(terminal_id) => {
70 let actual_log_path = log_path.clone();
72 self.terminal_log_files
73 .insert(terminal_id, actual_log_path.clone());
74 if terminal_id != predicted_terminal_id {
76 self.terminal_backing_files.remove(&predicted_terminal_id);
77 let backing_path =
78 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
79 self.terminal_backing_files
80 .insert(terminal_id, backing_path);
81 }
82
83 let buffer_id = self.create_terminal_buffer_attached(
85 terminal_id,
86 self.split_manager.active_split(),
87 );
88
89 self.set_active_buffer(buffer_id);
91
92 self.terminal_mode = true;
94 self.key_context = crate::input::keybindings::KeyContext::Terminal;
95
96 self.resize_visible_terminals();
98
99 let exit_key = self
101 .keybindings
102 .read()
103 .unwrap()
104 .find_keybinding_for_action(
105 "terminal_escape",
106 crate::input::keybindings::KeyContext::Terminal,
107 )
108 .unwrap_or_else(|| "Ctrl+Space".to_string());
109 self.set_status_message(
110 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
111 );
112 tracing::info!(
113 "Opened terminal {:?} with buffer {:?}",
114 terminal_id,
115 buffer_id
116 );
117 }
118 Err(e) => {
119 self.set_status_message(
120 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
121 );
122 tracing::error!("Failed to open terminal: {}", e);
123 }
124 }
125 }
126
127 pub(crate) fn create_terminal_buffer_attached(
129 &mut self,
130 terminal_id: TerminalId,
131 split_id: crate::model::event::LeafId,
132 ) -> BufferId {
133 let buffer_id = BufferId(self.next_buffer_id);
134 self.next_buffer_id += 1;
135
136 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
138
139 let backing_file = self
141 .terminal_backing_files
142 .get(&terminal_id)
143 .cloned()
144 .unwrap_or_else(|| {
145 let root = self.dir_context.terminal_dir_for(&self.working_dir);
146 if let Err(e) = self.filesystem.create_dir_all(&root) {
147 tracing::warn!("Failed to create terminal directory: {}", e);
148 }
149 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
150 });
151
152 if !self.filesystem.exists(&backing_file) {
155 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
156 tracing::warn!("Failed to create terminal backing file: {}", e);
157 }
158 }
159
160 self.terminal_backing_files
162 .insert(terminal_id, backing_file.clone());
163
164 let mut state = EditorState::new_with_path(
166 large_file_threshold,
167 std::sync::Arc::clone(&self.filesystem),
168 backing_file.clone(),
169 );
170 state.margins.configure_for_line_numbers(false);
172 self.buffers.insert(buffer_id, state);
173
174 let metadata = BufferMetadata::virtual_buffer(
177 format!("*Terminal {}*", terminal_id.0),
178 "terminal".into(),
179 false,
180 );
181 self.buffer_metadata.insert(buffer_id, metadata);
182
183 self.terminal_buffers.insert(buffer_id, terminal_id);
185
186 self.event_logs
188 .insert(buffer_id, crate::model::event::EventLog::new());
189
190 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
192 view_state.open_buffers.push(buffer_id);
193 view_state.viewport.line_wrap_enabled = false;
195 }
196
197 buffer_id
198 }
199
200 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
202 let buffer_id = BufferId(self.next_buffer_id);
203 self.next_buffer_id += 1;
204
205 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
207
208 let backing_file = self
209 .terminal_backing_files
210 .get(&terminal_id)
211 .cloned()
212 .unwrap_or_else(|| {
213 let root = self.dir_context.terminal_dir_for(&self.working_dir);
214 if let Err(e) = self.filesystem.create_dir_all(&root) {
215 tracing::warn!("Failed to create terminal directory: {}", e);
216 }
217 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
218 });
219
220 if !self.filesystem.exists(&backing_file) {
222 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
223 tracing::warn!("Failed to create terminal backing file: {}", e);
224 }
225 }
226
227 let mut state = EditorState::new_with_path(
229 large_file_threshold,
230 std::sync::Arc::clone(&self.filesystem),
231 backing_file.clone(),
232 );
233 state.margins.configure_for_line_numbers(false);
234 self.buffers.insert(buffer_id, state);
235
236 let metadata = BufferMetadata::virtual_buffer(
237 format!("*Terminal {}*", terminal_id.0),
238 "terminal".into(),
239 false,
240 );
241 self.buffer_metadata.insert(buffer_id, metadata);
242 self.terminal_buffers.insert(buffer_id, terminal_id);
243 self.event_logs
244 .insert(buffer_id, crate::model::event::EventLog::new());
245
246 buffer_id
247 }
248
249 pub fn close_terminal(&mut self) {
251 let buffer_id = self.active_buffer();
252
253 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
254 self.terminal_manager.close(terminal_id);
256 self.terminal_buffers.remove(&buffer_id);
257
258 let backing_file = self.terminal_backing_files.remove(&terminal_id);
260 if let Some(ref path) = backing_file {
261 #[allow(clippy::let_underscore_must_use)]
263 let _ = self.filesystem.remove_file(path);
264 }
265 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
267 if backing_file.as_ref() != Some(&log_file) {
268 #[allow(clippy::let_underscore_must_use)]
270 let _ = self.filesystem.remove_file(&log_file);
271 }
272 }
273
274 self.terminal_mode = false;
276 self.key_context = crate::input::keybindings::KeyContext::Normal;
277
278 if let Err(e) = self.close_buffer(buffer_id) {
280 tracing::warn!("Failed to close terminal buffer: {}", e);
281 }
282
283 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
284 } else {
285 self.set_status_message(t!("status.not_viewing_terminal").to_string());
286 }
287 }
288
289 pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
291 self.terminal_buffers.contains_key(&buffer_id)
292 }
293
294 pub fn get_terminal_id(&self, buffer_id: BufferId) -> Option<TerminalId> {
296 self.terminal_buffers.get(&buffer_id).copied()
297 }
298
299 pub fn get_active_terminal_state(
301 &self,
302 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
303 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
304 let handle = self.terminal_manager.get(*terminal_id)?;
305 handle.state.lock().ok()
306 }
307
308 pub fn send_terminal_input(&mut self, data: &[u8]) {
310 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
311 if let Some(handle) = self.terminal_manager.get(terminal_id) {
312 handle.write(data);
313 }
314 }
315 }
316
317 pub fn send_terminal_key(
319 &mut self,
320 code: crossterm::event::KeyCode,
321 modifiers: crossterm::event::KeyModifiers,
322 ) {
323 let app_cursor = self
324 .get_active_terminal_state()
325 .map(|s| s.is_app_cursor())
326 .unwrap_or(false);
327 if let Some(bytes) =
328 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
329 {
330 self.send_terminal_input(&bytes);
331 }
332 }
333
334 pub fn send_terminal_mouse(
336 &mut self,
337 col: u16,
338 row: u16,
339 kind: crate::input::handler::TerminalMouseEventKind,
340 modifiers: crossterm::event::KeyModifiers,
341 ) {
342 use crate::input::handler::TerminalMouseEventKind;
343
344 let use_sgr = self
346 .get_active_terminal_state()
347 .map(|s| s.uses_sgr_mouse())
348 .unwrap_or(true); let uses_alt_scroll = self
352 .get_active_terminal_state()
353 .map(|s| s.uses_alternate_scroll())
354 .unwrap_or(false);
355
356 if uses_alt_scroll {
357 match kind {
358 TerminalMouseEventKind::ScrollUp => {
359 for _ in 0..3 {
361 self.send_terminal_input(b"\x1b[A");
362 }
363 return;
364 }
365 TerminalMouseEventKind::ScrollDown => {
366 for _ in 0..3 {
368 self.send_terminal_input(b"\x1b[B");
369 }
370 return;
371 }
372 _ => {}
373 }
374 }
375
376 let bytes = if use_sgr {
378 encode_sgr_mouse(col, row, kind, modifiers)
379 } else {
380 encode_x10_mouse(col, row, kind, modifiers)
381 };
382
383 if let Some(bytes) = bytes {
384 self.send_terminal_input(&bytes);
385 }
386 }
387
388 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
391 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
392 if let Some(handle) = self.terminal_manager.get(terminal_id) {
393 if let Ok(state) = handle.state.lock() {
394 return state.is_alternate_screen();
395 }
396 }
397 }
398 false
399 }
400
401 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
403 let cols = self.terminal_width.saturating_sub(2).max(40);
406 let rows = self.terminal_height.saturating_sub(4).max(10);
407 (cols, rows)
408 }
409
410 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
412 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
413 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
414 handle.resize(cols, rows);
415 }
416 }
417 }
418
419 pub fn resize_visible_terminals(&mut self) {
422 let file_explorer_width = if self.file_explorer_visible {
424 (self.terminal_width as f32 * self.file_explorer_width_percent) as u16
425 } else {
426 0
427 };
428 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
429 let editor_area = ratatui::layout::Rect::new(
430 file_explorer_width,
431 1, editor_width,
433 self.terminal_height.saturating_sub(2), );
435
436 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
438
439 for (_split_id, buffer_id, split_area) in visible_buffers {
441 if self.terminal_buffers.contains_key(&buffer_id) {
442 let content_height = split_area.height.saturating_sub(2);
445 let content_width = split_area.width.saturating_sub(2);
446
447 if content_width > 0 && content_height > 0 {
448 self.resize_terminal(buffer_id, content_width, content_height);
449 }
450 }
451 }
452 }
453
454 pub fn handle_terminal_key(
456 &mut self,
457 code: crossterm::event::KeyCode,
458 modifiers: crossterm::event::KeyModifiers,
459 ) -> bool {
460 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
463 match code {
464 crossterm::event::KeyCode::Char(' ')
465 | crossterm::event::KeyCode::Char(']')
466 | crossterm::event::KeyCode::Char('`') => {
467 self.terminal_mode = false;
469 self.key_context = crate::input::keybindings::KeyContext::Normal;
470 self.sync_terminal_to_buffer(self.active_buffer());
471 self.set_status_message(
472 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
473 );
474 return true;
475 }
476 _ => {}
477 }
478 }
479
480 self.send_terminal_key(code, modifiers);
482 true
483 }
484
485 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
494 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
495 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
497 Some(path) => path.clone(),
498 None => return,
499 };
500
501 if let Some(handle) = self.terminal_manager.get(terminal_id) {
504 if let Ok(mut state) = handle.state.lock() {
505 if let Ok(metadata) = self.filesystem.metadata(&backing_file) {
508 state.set_backing_file_history_end(metadata.size);
509 }
510
511 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_file) {
513 use std::io::BufWriter;
514 let mut writer = BufWriter::new(&mut *file);
515 if let Err(e) = state.append_visible_screen(&mut writer) {
516 tracing::error!(
517 "Failed to append visible screen to backing file: {}",
518 e
519 );
520 }
521 }
522 }
523 }
524
525 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
527 if let Ok(new_state) = EditorState::from_file_with_languages(
528 &backing_file,
529 self.terminal_width,
530 self.terminal_height,
531 large_file_threshold,
532 &self.grammar_registry,
533 &self.config.languages,
534 std::sync::Arc::clone(&self.filesystem),
535 ) {
536 if let Some(state) = self.buffers.get_mut(&buffer_id) {
538 let total_bytes = new_state.buffer.total_bytes();
539 *state = new_state;
540 state.buffer.set_modified(false);
542 if let Some(view_state) = self
544 .split_view_states
545 .get_mut(&self.split_manager.active_split())
546 {
547 view_state.cursors.primary_mut().position = total_bytes;
548 }
549 }
550 }
551
552 if let Some(state) = self.buffers.get_mut(&buffer_id) {
554 state.editing_disabled = true;
555 state.margins.configure_for_line_numbers(false);
556 }
557
558 if let Some(view_state) = self
561 .split_view_states
562 .get_mut(&self.split_manager.active_split())
563 {
564 view_state.viewport.line_wrap_enabled = false;
565
566 view_state.viewport.clear_skip_ensure_visible();
570
571 if let Some(state) = self.buffers.get_mut(&buffer_id) {
573 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
574 }
575 }
576 }
577 }
578
579 pub fn enter_terminal_mode(&mut self) {
585 if self.is_terminal_buffer(self.active_buffer()) {
586 self.terminal_mode = true;
587 self.key_context = crate::input::keybindings::KeyContext::Terminal;
588
589 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
591 state.editing_disabled = false;
592 state.margins.configure_for_line_numbers(false);
593 }
594 if let Some(view_state) = self
595 .split_view_states
596 .get_mut(&self.split_manager.active_split())
597 {
598 view_state.viewport.line_wrap_enabled = false;
599 }
600
601 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
603 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
605 if let Some(handle) = self.terminal_manager.get(terminal_id) {
606 if let Ok(state) = handle.state.lock() {
607 let truncate_pos = state.backing_file_history_end();
608 if let Err(e) =
611 self.filesystem.set_file_length(backing_path, truncate_pos)
612 {
613 tracing::warn!("Failed to truncate terminal backing file: {}", e);
614 }
615 }
616 }
617 }
618
619 if let Some(handle) = self.terminal_manager.get(terminal_id) {
621 if let Ok(mut state) = handle.state.lock() {
622 state.scroll_to_bottom();
623 }
624 }
625 }
626
627 self.resize_visible_terminals();
629
630 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
631 }
632 }
633
634 pub fn get_terminal_content(
636 &self,
637 buffer_id: BufferId,
638 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
639 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
640 let handle = self.terminal_manager.get(*terminal_id)?;
641 let state = handle.state.lock().ok()?;
642
643 let (_, rows) = state.size();
644 let mut content = Vec::with_capacity(rows as usize);
645
646 for row in 0..rows {
647 content.push(state.get_line(row));
648 }
649
650 Some(content)
651 }
652}
653
654impl Editor {
655 pub fn is_terminal_mode(&self) -> bool {
657 self.terminal_mode
658 }
659
660 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
662 self.terminal_mode_resume.contains(&buffer_id)
663 }
664
665 pub fn is_keyboard_capture(&self) -> bool {
667 self.keyboard_capture
668 }
669
670 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
672 self.config.terminal.jump_to_end_on_output = value;
673 }
674
675 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
677 &self.terminal_manager
678 }
679
680 pub fn terminal_backing_files(
682 &self,
683 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
684 &self.terminal_backing_files
685 }
686
687 pub fn active_buffer_id(&self) -> BufferId {
689 self.active_buffer()
690 }
691
692 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
694 self.buffers
695 .get(&buffer_id)
696 .and_then(|state| state.buffer.to_string())
697 }
698
699 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
701 self.split_view_states
703 .values()
704 .find_map(|vs| {
705 if vs.keyed_states.contains_key(&buffer_id) {
706 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
707 } else {
708 None
709 }
710 })
711 .or_else(|| {
712 self.split_view_states
714 .values()
715 .map(|vs| vs.cursors.primary().position)
716 .next()
717 })
718 }
719
720 pub fn render_terminal_splits(
726 &self,
727 frame: &mut ratatui::Frame,
728 split_areas: &[(
729 crate::model::event::LeafId,
730 BufferId,
731 ratatui::layout::Rect,
732 ratatui::layout::Rect,
733 usize,
734 usize,
735 )],
736 ) {
737 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
738 split_areas
739 {
740 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
742 let is_active = *buffer_id == self.active_buffer();
746 if is_active && !self.terminal_mode {
747 continue;
749 }
750 if let Some(handle) = self.terminal_manager.get(terminal_id) {
752 if let Ok(state) = handle.state.lock() {
753 let cursor_pos = state.cursor_position();
754 let cursor_visible =
756 state.cursor_visible() && is_active && self.terminal_mode;
757 let (_, rows) = state.size();
758
759 let mut content = Vec::with_capacity(rows as usize);
761 for row in 0..rows {
762 content.push(state.get_line(row));
763 }
764
765 frame.render_widget(ratatui::widgets::Clear, *content_rect);
767
768 render::render_terminal_content(
770 &content,
771 cursor_pos,
772 cursor_visible,
773 *content_rect,
774 frame.buffer_mut(),
775 self.theme.terminal_fg,
776 self.theme.terminal_bg,
777 );
778 }
779 }
780 }
781 }
782 }
783}
784
785pub mod render {
787 use crate::services::terminal::TerminalCell;
788 use ratatui::buffer::Buffer;
789 use ratatui::layout::Rect;
790 use ratatui::style::{Color, Modifier, Style};
791
792 pub fn render_terminal_content(
794 content: &[Vec<TerminalCell>],
795 cursor_pos: (u16, u16),
796 cursor_visible: bool,
797 area: Rect,
798 buf: &mut Buffer,
799 default_fg: Color,
800 default_bg: Color,
801 ) {
802 for (row_idx, row) in content.iter().enumerate() {
803 if row_idx as u16 >= area.height {
804 break;
805 }
806
807 let y = area.y + row_idx as u16;
808
809 for (col_idx, cell) in row.iter().enumerate() {
810 if col_idx as u16 >= area.width {
811 break;
812 }
813
814 let x = area.x + col_idx as u16;
815
816 let mut style = Style::default().fg(default_fg).bg(default_bg);
818
819 if let Some((r, g, b)) = cell.fg {
821 style = style.fg(Color::Rgb(r, g, b));
822 }
823
824 if let Some((r, g, b)) = cell.bg {
825 style = style.bg(Color::Rgb(r, g, b));
826 }
827
828 if cell.bold {
830 style = style.add_modifier(Modifier::BOLD);
831 }
832 if cell.italic {
833 style = style.add_modifier(Modifier::ITALIC);
834 }
835 if cell.underline {
836 style = style.add_modifier(Modifier::UNDERLINED);
837 }
838 if cell.inverse {
839 style = style.add_modifier(Modifier::REVERSED);
840 }
841
842 if cursor_visible
844 && row_idx as u16 == cursor_pos.1
845 && col_idx as u16 == cursor_pos.0
846 {
847 style = style.add_modifier(Modifier::REVERSED);
848 }
849
850 buf.set_string(x, y, cell.c.to_string(), style);
851 }
852 }
853 }
854}
855
856fn encode_sgr_mouse(
859 col: u16,
860 row: u16,
861 kind: crate::input::handler::TerminalMouseEventKind,
862 modifiers: crossterm::event::KeyModifiers,
863) -> Option<Vec<u8>> {
864 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
865
866 let cx = col + 1;
868 let cy = row + 1;
869
870 let (button_code, is_release) = match kind {
872 TerminalMouseEventKind::Down(btn) => {
873 let code = match btn {
874 TerminalMouseButton::Left => 0,
875 TerminalMouseButton::Middle => 1,
876 TerminalMouseButton::Right => 2,
877 };
878 (code, false)
879 }
880 TerminalMouseEventKind::Up(btn) => {
881 let code = match btn {
882 TerminalMouseButton::Left => 0,
883 TerminalMouseButton::Middle => 1,
884 TerminalMouseButton::Right => 2,
885 };
886 (code, true)
887 }
888 TerminalMouseEventKind::Drag(btn) => {
889 let code = match btn {
890 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
894 (code, false)
895 }
896 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
898 TerminalMouseEventKind::ScrollDown => (65, false),
899 };
900
901 let mut cb = button_code;
903 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
904 cb += 4;
905 }
906 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
907 cb += 8;
908 }
909 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
910 cb += 16;
911 }
912
913 let terminator = if is_release { 'm' } else { 'M' };
915 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
916}
917
918fn encode_x10_mouse(
921 col: u16,
922 row: u16,
923 kind: crate::input::handler::TerminalMouseEventKind,
924 modifiers: crossterm::event::KeyModifiers,
925) -> Option<Vec<u8>> {
926 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
927
928 let cx = (col.min(222) + 1 + 32) as u8;
931 let cy = (row.min(222) + 1 + 32) as u8;
932
933 let button_code: u8 = match kind {
935 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
936 TerminalMouseButton::Left => 0,
937 TerminalMouseButton::Middle => 1,
938 TerminalMouseButton::Right => 2,
939 },
940 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
942 TerminalMouseEventKind::ScrollUp => 64,
943 TerminalMouseEventKind::ScrollDown => 65,
944 };
945
946 let mut cb = button_code;
948 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
949 cb += 32; }
951 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
952 cb += 4;
953 }
954 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
955 cb += 8;
956 }
957 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
958 cb += 16;
959 }
960
961 let cb = cb + 32;
963
964 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
965}