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 .find_keybinding_for_action(
103 "terminal_escape",
104 crate::input::keybindings::KeyContext::Terminal,
105 )
106 .unwrap_or_else(|| "Ctrl+Space".to_string());
107 self.set_status_message(
108 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
109 );
110 tracing::info!(
111 "Opened terminal {:?} with buffer {:?}",
112 terminal_id,
113 buffer_id
114 );
115 }
116 Err(e) => {
117 self.set_status_message(
118 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
119 );
120 tracing::error!("Failed to open terminal: {}", e);
121 }
122 }
123 }
124
125 pub(crate) fn create_terminal_buffer_attached(
127 &mut self,
128 terminal_id: TerminalId,
129 split_id: crate::model::event::LeafId,
130 ) -> BufferId {
131 let buffer_id = BufferId(self.next_buffer_id);
132 self.next_buffer_id += 1;
133
134 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
136
137 let backing_file = self
139 .terminal_backing_files
140 .get(&terminal_id)
141 .cloned()
142 .unwrap_or_else(|| {
143 let root = self.dir_context.terminal_dir_for(&self.working_dir);
144 if let Err(e) = self.filesystem.create_dir_all(&root) {
145 tracing::warn!("Failed to create terminal directory: {}", e);
146 }
147 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
148 });
149
150 if !self.filesystem.exists(&backing_file) {
153 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
154 tracing::warn!("Failed to create terminal backing file: {}", e);
155 }
156 }
157
158 self.terminal_backing_files
160 .insert(terminal_id, backing_file.clone());
161
162 let mut state = EditorState::new_with_path(
164 large_file_threshold,
165 std::sync::Arc::clone(&self.filesystem),
166 backing_file.clone(),
167 );
168 state.margins.configure_for_line_numbers(false);
170 self.buffers.insert(buffer_id, state);
171
172 let metadata = BufferMetadata::virtual_buffer(
175 format!("*Terminal {}*", terminal_id.0),
176 "terminal".into(),
177 false,
178 );
179 self.buffer_metadata.insert(buffer_id, metadata);
180
181 self.terminal_buffers.insert(buffer_id, terminal_id);
183
184 self.event_logs
186 .insert(buffer_id, crate::model::event::EventLog::new());
187
188 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
190 view_state.open_buffers.push(buffer_id);
191 view_state.viewport.line_wrap_enabled = false;
193 }
194
195 buffer_id
196 }
197
198 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
200 let buffer_id = BufferId(self.next_buffer_id);
201 self.next_buffer_id += 1;
202
203 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
205
206 let backing_file = self
207 .terminal_backing_files
208 .get(&terminal_id)
209 .cloned()
210 .unwrap_or_else(|| {
211 let root = self.dir_context.terminal_dir_for(&self.working_dir);
212 if let Err(e) = self.filesystem.create_dir_all(&root) {
213 tracing::warn!("Failed to create terminal directory: {}", e);
214 }
215 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
216 });
217
218 if !self.filesystem.exists(&backing_file) {
220 if let Err(e) = self.filesystem.write_file(&backing_file, &[]) {
221 tracing::warn!("Failed to create terminal backing file: {}", e);
222 }
223 }
224
225 let mut state = EditorState::new_with_path(
227 large_file_threshold,
228 std::sync::Arc::clone(&self.filesystem),
229 backing_file.clone(),
230 );
231 state.margins.configure_for_line_numbers(false);
232 self.buffers.insert(buffer_id, state);
233
234 let metadata = BufferMetadata::virtual_buffer(
235 format!("*Terminal {}*", terminal_id.0),
236 "terminal".into(),
237 false,
238 );
239 self.buffer_metadata.insert(buffer_id, metadata);
240 self.terminal_buffers.insert(buffer_id, terminal_id);
241 self.event_logs
242 .insert(buffer_id, crate::model::event::EventLog::new());
243
244 buffer_id
245 }
246
247 pub fn close_terminal(&mut self) {
249 let buffer_id = self.active_buffer();
250
251 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
252 self.terminal_manager.close(terminal_id);
254 self.terminal_buffers.remove(&buffer_id);
255
256 let backing_file = self.terminal_backing_files.remove(&terminal_id);
258 if let Some(ref path) = backing_file {
259 #[allow(clippy::let_underscore_must_use)]
261 let _ = self.filesystem.remove_file(path);
262 }
263 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
265 if backing_file.as_ref() != Some(&log_file) {
266 #[allow(clippy::let_underscore_must_use)]
268 let _ = self.filesystem.remove_file(&log_file);
269 }
270 }
271
272 self.terminal_mode = false;
274 self.key_context = crate::input::keybindings::KeyContext::Normal;
275
276 if let Err(e) = self.close_buffer(buffer_id) {
278 tracing::warn!("Failed to close terminal buffer: {}", e);
279 }
280
281 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
282 } else {
283 self.set_status_message(t!("status.not_viewing_terminal").to_string());
284 }
285 }
286
287 pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
289 self.terminal_buffers.contains_key(&buffer_id)
290 }
291
292 pub fn get_terminal_id(&self, buffer_id: BufferId) -> Option<TerminalId> {
294 self.terminal_buffers.get(&buffer_id).copied()
295 }
296
297 pub fn get_active_terminal_state(
299 &self,
300 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
301 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
302 let handle = self.terminal_manager.get(*terminal_id)?;
303 handle.state.lock().ok()
304 }
305
306 pub fn send_terminal_input(&mut self, data: &[u8]) {
308 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
309 if let Some(handle) = self.terminal_manager.get(terminal_id) {
310 handle.write(data);
311 }
312 }
313 }
314
315 pub fn send_terminal_key(
317 &mut self,
318 code: crossterm::event::KeyCode,
319 modifiers: crossterm::event::KeyModifiers,
320 ) {
321 let app_cursor = self
322 .get_active_terminal_state()
323 .map(|s| s.is_app_cursor())
324 .unwrap_or(false);
325 if let Some(bytes) =
326 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
327 {
328 self.send_terminal_input(&bytes);
329 }
330 }
331
332 pub fn send_terminal_mouse(
334 &mut self,
335 col: u16,
336 row: u16,
337 kind: crate::input::handler::TerminalMouseEventKind,
338 modifiers: crossterm::event::KeyModifiers,
339 ) {
340 use crate::input::handler::TerminalMouseEventKind;
341
342 let use_sgr = self
344 .get_active_terminal_state()
345 .map(|s| s.uses_sgr_mouse())
346 .unwrap_or(true); let uses_alt_scroll = self
350 .get_active_terminal_state()
351 .map(|s| s.uses_alternate_scroll())
352 .unwrap_or(false);
353
354 if uses_alt_scroll {
355 match kind {
356 TerminalMouseEventKind::ScrollUp => {
357 for _ in 0..3 {
359 self.send_terminal_input(b"\x1b[A");
360 }
361 return;
362 }
363 TerminalMouseEventKind::ScrollDown => {
364 for _ in 0..3 {
366 self.send_terminal_input(b"\x1b[B");
367 }
368 return;
369 }
370 _ => {}
371 }
372 }
373
374 let bytes = if use_sgr {
376 encode_sgr_mouse(col, row, kind, modifiers)
377 } else {
378 encode_x10_mouse(col, row, kind, modifiers)
379 };
380
381 if let Some(bytes) = bytes {
382 self.send_terminal_input(&bytes);
383 }
384 }
385
386 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
389 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
390 if let Some(handle) = self.terminal_manager.get(terminal_id) {
391 if let Ok(state) = handle.state.lock() {
392 return state.is_alternate_screen();
393 }
394 }
395 }
396 false
397 }
398
399 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
401 let cols = self.terminal_width.saturating_sub(2).max(40);
404 let rows = self.terminal_height.saturating_sub(4).max(10);
405 (cols, rows)
406 }
407
408 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
410 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
411 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
412 handle.resize(cols, rows);
413 }
414 }
415 }
416
417 pub fn resize_visible_terminals(&mut self) {
420 let file_explorer_width = if self.file_explorer_visible {
422 (self.terminal_width as f32 * self.file_explorer_width_percent) as u16
423 } else {
424 0
425 };
426 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
427 let editor_area = ratatui::layout::Rect::new(
428 file_explorer_width,
429 1, editor_width,
431 self.terminal_height.saturating_sub(2), );
433
434 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
436
437 for (_split_id, buffer_id, split_area) in visible_buffers {
439 if self.terminal_buffers.contains_key(&buffer_id) {
440 let content_height = split_area.height.saturating_sub(2);
443 let content_width = split_area.width.saturating_sub(2);
444
445 if content_width > 0 && content_height > 0 {
446 self.resize_terminal(buffer_id, content_width, content_height);
447 }
448 }
449 }
450 }
451
452 pub fn handle_terminal_key(
454 &mut self,
455 code: crossterm::event::KeyCode,
456 modifiers: crossterm::event::KeyModifiers,
457 ) -> bool {
458 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
461 match code {
462 crossterm::event::KeyCode::Char(' ')
463 | crossterm::event::KeyCode::Char(']')
464 | crossterm::event::KeyCode::Char('`') => {
465 self.terminal_mode = false;
467 self.key_context = crate::input::keybindings::KeyContext::Normal;
468 self.sync_terminal_to_buffer(self.active_buffer());
469 self.set_status_message(
470 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
471 );
472 return true;
473 }
474 _ => {}
475 }
476 }
477
478 self.send_terminal_key(code, modifiers);
480 true
481 }
482
483 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
492 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
493 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
495 Some(path) => path.clone(),
496 None => return,
497 };
498
499 if let Some(handle) = self.terminal_manager.get(terminal_id) {
502 if let Ok(mut state) = handle.state.lock() {
503 if let Ok(metadata) = self.filesystem.metadata(&backing_file) {
506 state.set_backing_file_history_end(metadata.size);
507 }
508
509 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_file) {
511 use std::io::BufWriter;
512 let mut writer = BufWriter::new(&mut *file);
513 if let Err(e) = state.append_visible_screen(&mut writer) {
514 tracing::error!(
515 "Failed to append visible screen to backing file: {}",
516 e
517 );
518 }
519 }
520 }
521 }
522
523 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
525 if let Ok(new_state) = EditorState::from_file_with_languages(
526 &backing_file,
527 self.terminal_width,
528 self.terminal_height,
529 large_file_threshold,
530 &self.grammar_registry,
531 &self.config.languages,
532 std::sync::Arc::clone(&self.filesystem),
533 ) {
534 if let Some(state) = self.buffers.get_mut(&buffer_id) {
536 let total_bytes = new_state.buffer.total_bytes();
537 *state = new_state;
538 state.buffer.set_modified(false);
540 if let Some(view_state) = self
542 .split_view_states
543 .get_mut(&self.split_manager.active_split())
544 {
545 view_state.cursors.primary_mut().position = total_bytes;
546 }
547 }
548 }
549
550 if let Some(state) = self.buffers.get_mut(&buffer_id) {
552 state.editing_disabled = true;
553 state.margins.configure_for_line_numbers(false);
554 }
555
556 if let Some(view_state) = self
559 .split_view_states
560 .get_mut(&self.split_manager.active_split())
561 {
562 view_state.viewport.line_wrap_enabled = false;
563
564 view_state.viewport.clear_skip_ensure_visible();
568
569 if let Some(state) = self.buffers.get_mut(&buffer_id) {
571 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
572 }
573 }
574 }
575 }
576
577 pub fn enter_terminal_mode(&mut self) {
583 if self.is_terminal_buffer(self.active_buffer()) {
584 self.terminal_mode = true;
585 self.key_context = crate::input::keybindings::KeyContext::Terminal;
586
587 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
589 state.editing_disabled = false;
590 state.margins.configure_for_line_numbers(false);
591 }
592 if let Some(view_state) = self
593 .split_view_states
594 .get_mut(&self.split_manager.active_split())
595 {
596 view_state.viewport.line_wrap_enabled = false;
597 }
598
599 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
601 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
603 if let Some(handle) = self.terminal_manager.get(terminal_id) {
604 if let Ok(state) = handle.state.lock() {
605 let truncate_pos = state.backing_file_history_end();
606 if let Err(e) =
609 self.filesystem.set_file_length(backing_path, truncate_pos)
610 {
611 tracing::warn!("Failed to truncate terminal backing file: {}", e);
612 }
613 }
614 }
615 }
616
617 if let Some(handle) = self.terminal_manager.get(terminal_id) {
619 if let Ok(mut state) = handle.state.lock() {
620 state.scroll_to_bottom();
621 }
622 }
623 }
624
625 self.resize_visible_terminals();
627
628 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
629 }
630 }
631
632 pub fn get_terminal_content(
634 &self,
635 buffer_id: BufferId,
636 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
637 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
638 let handle = self.terminal_manager.get(*terminal_id)?;
639 let state = handle.state.lock().ok()?;
640
641 let (_, rows) = state.size();
642 let mut content = Vec::with_capacity(rows as usize);
643
644 for row in 0..rows {
645 content.push(state.get_line(row));
646 }
647
648 Some(content)
649 }
650}
651
652impl Editor {
653 pub fn is_terminal_mode(&self) -> bool {
655 self.terminal_mode
656 }
657
658 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
660 self.terminal_mode_resume.contains(&buffer_id)
661 }
662
663 pub fn is_keyboard_capture(&self) -> bool {
665 self.keyboard_capture
666 }
667
668 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
670 self.config.terminal.jump_to_end_on_output = value;
671 }
672
673 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
675 &self.terminal_manager
676 }
677
678 pub fn terminal_backing_files(
680 &self,
681 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
682 &self.terminal_backing_files
683 }
684
685 pub fn active_buffer_id(&self) -> BufferId {
687 self.active_buffer()
688 }
689
690 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
692 self.buffers
693 .get(&buffer_id)
694 .and_then(|state| state.buffer.to_string())
695 }
696
697 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
699 self.split_view_states
701 .values()
702 .find_map(|vs| {
703 if vs.keyed_states.contains_key(&buffer_id) {
704 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
705 } else {
706 None
707 }
708 })
709 .or_else(|| {
710 self.split_view_states
712 .values()
713 .map(|vs| vs.cursors.primary().position)
714 .next()
715 })
716 }
717
718 pub fn render_terminal_splits(
724 &self,
725 frame: &mut ratatui::Frame,
726 split_areas: &[(
727 crate::model::event::LeafId,
728 BufferId,
729 ratatui::layout::Rect,
730 ratatui::layout::Rect,
731 usize,
732 usize,
733 )],
734 ) {
735 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
736 split_areas
737 {
738 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
740 let is_active = *buffer_id == self.active_buffer();
744 if is_active && !self.terminal_mode {
745 continue;
747 }
748 if let Some(handle) = self.terminal_manager.get(terminal_id) {
750 if let Ok(state) = handle.state.lock() {
751 let cursor_pos = state.cursor_position();
752 let cursor_visible =
754 state.cursor_visible() && is_active && self.terminal_mode;
755 let (_, rows) = state.size();
756
757 let mut content = Vec::with_capacity(rows as usize);
759 for row in 0..rows {
760 content.push(state.get_line(row));
761 }
762
763 frame.render_widget(ratatui::widgets::Clear, *content_rect);
765
766 render::render_terminal_content(
768 &content,
769 cursor_pos,
770 cursor_visible,
771 *content_rect,
772 frame.buffer_mut(),
773 self.theme.terminal_fg,
774 self.theme.terminal_bg,
775 );
776 }
777 }
778 }
779 }
780 }
781}
782
783pub mod render {
785 use crate::services::terminal::TerminalCell;
786 use ratatui::buffer::Buffer;
787 use ratatui::layout::Rect;
788 use ratatui::style::{Color, Modifier, Style};
789
790 pub fn render_terminal_content(
792 content: &[Vec<TerminalCell>],
793 cursor_pos: (u16, u16),
794 cursor_visible: bool,
795 area: Rect,
796 buf: &mut Buffer,
797 default_fg: Color,
798 default_bg: Color,
799 ) {
800 for (row_idx, row) in content.iter().enumerate() {
801 if row_idx as u16 >= area.height {
802 break;
803 }
804
805 let y = area.y + row_idx as u16;
806
807 for (col_idx, cell) in row.iter().enumerate() {
808 if col_idx as u16 >= area.width {
809 break;
810 }
811
812 let x = area.x + col_idx as u16;
813
814 let mut style = Style::default().fg(default_fg).bg(default_bg);
816
817 if let Some((r, g, b)) = cell.fg {
819 style = style.fg(Color::Rgb(r, g, b));
820 }
821
822 if let Some((r, g, b)) = cell.bg {
823 style = style.bg(Color::Rgb(r, g, b));
824 }
825
826 if cell.bold {
828 style = style.add_modifier(Modifier::BOLD);
829 }
830 if cell.italic {
831 style = style.add_modifier(Modifier::ITALIC);
832 }
833 if cell.underline {
834 style = style.add_modifier(Modifier::UNDERLINED);
835 }
836 if cell.inverse {
837 style = style.add_modifier(Modifier::REVERSED);
838 }
839
840 if cursor_visible
842 && row_idx as u16 == cursor_pos.1
843 && col_idx as u16 == cursor_pos.0
844 {
845 style = style.add_modifier(Modifier::REVERSED);
846 }
847
848 buf.set_string(x, y, cell.c.to_string(), style);
849 }
850 }
851 }
852}
853
854fn encode_sgr_mouse(
857 col: u16,
858 row: u16,
859 kind: crate::input::handler::TerminalMouseEventKind,
860 modifiers: crossterm::event::KeyModifiers,
861) -> Option<Vec<u8>> {
862 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
863
864 let cx = col + 1;
866 let cy = row + 1;
867
868 let (button_code, is_release) = match kind {
870 TerminalMouseEventKind::Down(btn) => {
871 let code = match btn {
872 TerminalMouseButton::Left => 0,
873 TerminalMouseButton::Middle => 1,
874 TerminalMouseButton::Right => 2,
875 };
876 (code, false)
877 }
878 TerminalMouseEventKind::Up(btn) => {
879 let code = match btn {
880 TerminalMouseButton::Left => 0,
881 TerminalMouseButton::Middle => 1,
882 TerminalMouseButton::Right => 2,
883 };
884 (code, true)
885 }
886 TerminalMouseEventKind::Drag(btn) => {
887 let code = match btn {
888 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
892 (code, false)
893 }
894 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
896 TerminalMouseEventKind::ScrollDown => (65, false),
897 };
898
899 let mut cb = button_code;
901 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
902 cb += 4;
903 }
904 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
905 cb += 8;
906 }
907 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
908 cb += 16;
909 }
910
911 let terminator = if is_release { 'm' } else { 'M' };
913 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
914}
915
916fn encode_x10_mouse(
919 col: u16,
920 row: u16,
921 kind: crate::input::handler::TerminalMouseEventKind,
922 modifiers: crossterm::event::KeyModifiers,
923) -> Option<Vec<u8>> {
924 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
925
926 let cx = (col.min(222) + 1 + 32) as u8;
929 let cy = (row.min(222) + 1 + 32) as u8;
930
931 let button_code: u8 = match kind {
933 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
934 TerminalMouseButton::Left => 0,
935 TerminalMouseButton::Middle => 1,
936 TerminalMouseButton::Right => 2,
937 },
938 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
940 TerminalMouseEventKind::ScrollUp => 64,
941 TerminalMouseEventKind::ScrollDown => 65,
942 };
943
944 let mut cb = button_code;
946 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
947 cb += 32; }
949 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
950 cb += 4;
951 }
952 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
953 cb += 8;
954 }
955 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
956 cb += 16;
957 }
958
959 let cb = cb + 32;
961
962 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
963}