1use super::{BufferId, BufferMetadata, Editor};
27use crate::services::authority::TerminalWrapper;
28use crate::services::terminal::TerminalId;
29use crate::state::EditorState;
30use rust_i18n::t;
31
32impl Editor {
33 pub(crate) fn resolved_terminal_wrapper(&self) -> TerminalWrapper {
41 self.authority
42 .terminal_wrapper
43 .clone()
44 .with_user_shell_override(self.config.terminal.shell.as_ref())
45 }
46
47 pub fn open_terminal(&mut self) {
49 let (cols, rows) = self.get_terminal_dimensions();
51
52 if let Some(ref bridge) = self.async_bridge {
54 self.terminal_manager.set_async_bridge(bridge.clone());
55 }
56
57 let terminal_root = self.dir_context.terminal_dir_for(&self.working_dir);
59 if let Err(e) = self.authority.filesystem.create_dir_all(&terminal_root) {
60 tracing::warn!("Failed to create terminal directory: {}", e);
61 }
62 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
64 let log_path =
65 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
66 let backing_path =
67 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
68 self.terminal_backing_files
70 .insert(predicted_terminal_id, backing_path);
71
72 let backing_path_for_spawn = self
74 .terminal_backing_files
75 .get(&predicted_terminal_id)
76 .cloned();
77 match self.terminal_manager.spawn(
78 cols,
79 rows,
80 Some(self.working_dir.clone()),
81 Some(log_path.clone()),
82 backing_path_for_spawn,
83 self.resolved_terminal_wrapper(),
84 ) {
85 Ok(terminal_id) => {
86 let actual_log_path = log_path.clone();
88 self.terminal_log_files
89 .insert(terminal_id, actual_log_path.clone());
90 if terminal_id != predicted_terminal_id {
92 self.terminal_backing_files.remove(&predicted_terminal_id);
93 let backing_path =
94 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
95 self.terminal_backing_files
96 .insert(terminal_id, backing_path);
97 }
98
99 let buffer_id = self.create_terminal_buffer_attached(
101 terminal_id,
102 self.split_manager.active_split(),
103 );
104
105 self.set_active_buffer(buffer_id);
107
108 self.terminal_mode = true;
110 self.key_context = crate::input::keybindings::KeyContext::Terminal;
111
112 self.resize_visible_terminals();
114
115 let exit_key = self
117 .keybindings
118 .read()
119 .unwrap()
120 .find_keybinding_for_action(
121 "terminal_escape",
122 crate::input::keybindings::KeyContext::Terminal,
123 )
124 .unwrap_or_else(|| "Ctrl+Space".to_string());
125 self.set_status_message(
126 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
127 );
128 tracing::info!(
129 "Opened terminal {:?} with buffer {:?}",
130 terminal_id,
131 buffer_id
132 );
133 }
134 Err(e) => {
135 self.set_status_message(
136 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
137 );
138 tracing::error!("Failed to open terminal: {}", e);
139 }
140 }
141 }
142
143 pub(crate) fn create_terminal_buffer_attached(
145 &mut self,
146 terminal_id: TerminalId,
147 split_id: crate::model::event::LeafId,
148 ) -> BufferId {
149 let buffer_id = BufferId(self.next_buffer_id);
150 self.next_buffer_id += 1;
151
152 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
154
155 let backing_file = self
157 .terminal_backing_files
158 .get(&terminal_id)
159 .cloned()
160 .unwrap_or_else(|| {
161 let root = self.dir_context.terminal_dir_for(&self.working_dir);
162 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
163 tracing::warn!("Failed to create terminal directory: {}", e);
164 }
165 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
166 });
167
168 if !self.authority.filesystem.exists(&backing_file) {
171 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
172 tracing::warn!("Failed to create terminal backing file: {}", e);
173 }
174 }
175
176 self.terminal_backing_files
178 .insert(terminal_id, backing_file.clone());
179
180 let mut state = EditorState::new_with_path(
182 large_file_threshold,
183 std::sync::Arc::clone(&self.authority.filesystem),
184 backing_file.clone(),
185 );
186 state.margins.configure_for_line_numbers(false);
188 self.buffers.insert(buffer_id, state);
189
190 let metadata = BufferMetadata::virtual_buffer(
193 format!("*Terminal {}*", terminal_id.0),
194 "terminal".into(),
195 false,
196 );
197 self.buffer_metadata.insert(buffer_id, metadata);
198
199 self.terminal_buffers.insert(buffer_id, terminal_id);
201
202 self.event_logs
204 .insert(buffer_id, crate::model::event::EventLog::new());
205
206 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
208 view_state.add_buffer(buffer_id);
209 view_state.viewport.line_wrap_enabled = false;
211 }
212
213 buffer_id
214 }
215
216 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
218 let buffer_id = BufferId(self.next_buffer_id);
219 self.next_buffer_id += 1;
220
221 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
223
224 let backing_file = self
225 .terminal_backing_files
226 .get(&terminal_id)
227 .cloned()
228 .unwrap_or_else(|| {
229 let root = self.dir_context.terminal_dir_for(&self.working_dir);
230 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
231 tracing::warn!("Failed to create terminal directory: {}", e);
232 }
233 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
234 });
235
236 if !self.authority.filesystem.exists(&backing_file) {
238 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
239 tracing::warn!("Failed to create terminal backing file: {}", e);
240 }
241 }
242
243 let mut state = EditorState::new_with_path(
245 large_file_threshold,
246 std::sync::Arc::clone(&self.authority.filesystem),
247 backing_file.clone(),
248 );
249 state.margins.configure_for_line_numbers(false);
250 self.buffers.insert(buffer_id, state);
251
252 let metadata = BufferMetadata::virtual_buffer(
253 format!("*Terminal {}*", terminal_id.0),
254 "terminal".into(),
255 false,
256 );
257 self.buffer_metadata.insert(buffer_id, metadata);
258 self.terminal_buffers.insert(buffer_id, terminal_id);
259 self.event_logs
260 .insert(buffer_id, crate::model::event::EventLog::new());
261
262 buffer_id
263 }
264
265 pub fn close_terminal(&mut self) {
267 let buffer_id = self.active_buffer();
268
269 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
270 self.terminal_manager.close(terminal_id);
272 self.terminal_buffers.remove(&buffer_id);
273 self.ephemeral_terminals.remove(&terminal_id);
274
275 let backing_file = self.terminal_backing_files.remove(&terminal_id);
277 if let Some(ref path) = backing_file {
278 #[allow(clippy::let_underscore_must_use)]
280 let _ = self.authority.filesystem.remove_file(path);
281 }
282 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
284 if backing_file.as_ref() != Some(&log_file) {
285 #[allow(clippy::let_underscore_must_use)]
287 let _ = self.authority.filesystem.remove_file(&log_file);
288 }
289 }
290
291 self.terminal_mode = false;
293 self.key_context = crate::input::keybindings::KeyContext::Normal;
294
295 if let Err(e) = self.close_buffer(buffer_id) {
297 tracing::warn!("Failed to close terminal buffer: {}", e);
298 }
299
300 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
301 } else {
302 self.set_status_message(t!("status.not_viewing_terminal").to_string());
303 }
304 }
305
306 pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
308 self.terminal_buffers.contains_key(&buffer_id)
309 }
310
311 pub fn get_terminal_id(&self, buffer_id: BufferId) -> Option<TerminalId> {
313 self.terminal_buffers.get(&buffer_id).copied()
314 }
315
316 pub fn get_active_terminal_state(
318 &self,
319 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
320 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
321 let handle = self.terminal_manager.get(*terminal_id)?;
322 handle.state.lock().ok()
323 }
324
325 pub fn send_terminal_input(&mut self, data: &[u8]) {
327 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
328 if let Some(handle) = self.terminal_manager.get(terminal_id) {
329 handle.write(data);
330 }
331 }
332 }
333
334 pub fn send_terminal_key(
336 &mut self,
337 code: crossterm::event::KeyCode,
338 modifiers: crossterm::event::KeyModifiers,
339 ) {
340 let app_cursor = self
341 .get_active_terminal_state()
342 .map(|s| s.is_app_cursor())
343 .unwrap_or(false);
344 if let Some(bytes) =
345 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
346 {
347 self.send_terminal_input(&bytes);
348 }
349 }
350
351 pub fn send_terminal_mouse(
353 &mut self,
354 col: u16,
355 row: u16,
356 kind: crate::input::handler::TerminalMouseEventKind,
357 modifiers: crossterm::event::KeyModifiers,
358 ) {
359 use crate::input::handler::TerminalMouseEventKind;
360
361 let use_sgr = self
363 .get_active_terminal_state()
364 .map(|s| s.uses_sgr_mouse())
365 .unwrap_or(true); let uses_alt_scroll = self
369 .get_active_terminal_state()
370 .map(|s| s.uses_alternate_scroll())
371 .unwrap_or(false);
372
373 if uses_alt_scroll {
374 match kind {
375 TerminalMouseEventKind::ScrollUp => {
376 for _ in 0..3 {
378 self.send_terminal_input(b"\x1b[A");
379 }
380 return;
381 }
382 TerminalMouseEventKind::ScrollDown => {
383 for _ in 0..3 {
385 self.send_terminal_input(b"\x1b[B");
386 }
387 return;
388 }
389 _ => {}
390 }
391 }
392
393 let bytes = if use_sgr {
395 encode_sgr_mouse(col, row, kind, modifiers)
396 } else {
397 encode_x10_mouse(col, row, kind, modifiers)
398 };
399
400 if let Some(bytes) = bytes {
401 self.send_terminal_input(&bytes);
402 }
403 }
404
405 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
408 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
409 if let Some(handle) = self.terminal_manager.get(terminal_id) {
410 if let Ok(state) = handle.state.lock() {
411 return state.is_alternate_screen();
412 }
413 }
414 }
415 false
416 }
417
418 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
420 let cols = self.terminal_width.saturating_sub(2).max(40);
423 let rows = self.terminal_height.saturating_sub(4).max(10);
424 (cols, rows)
425 }
426
427 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
429 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
430 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
431 handle.resize(cols, rows);
432 }
433 }
434 }
435
436 pub fn resize_visible_terminals(&mut self) {
439 let file_explorer_width = if self.file_explorer_visible {
441 self.file_explorer_width.to_cols(self.terminal_width)
442 } else {
443 0
444 };
445 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
446 let editor_area = ratatui::layout::Rect::new(
447 file_explorer_width,
448 1, editor_width,
450 self.terminal_height.saturating_sub(2), );
452
453 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
455
456 for (_split_id, buffer_id, split_area) in visible_buffers {
458 if self.terminal_buffers.contains_key(&buffer_id) {
459 let content_height = split_area.height.saturating_sub(2);
462 let content_width = split_area.width.saturating_sub(2);
463
464 if content_width > 0 && content_height > 0 {
465 self.resize_terminal(buffer_id, content_width, content_height);
466 }
467 }
468 }
469 }
470
471 pub fn handle_terminal_key(
473 &mut self,
474 code: crossterm::event::KeyCode,
475 modifiers: crossterm::event::KeyModifiers,
476 ) -> bool {
477 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
480 match code {
481 crossterm::event::KeyCode::Char(' ')
482 | crossterm::event::KeyCode::Char(']')
483 | crossterm::event::KeyCode::Char('`') => {
484 self.terminal_mode = false;
486 self.key_context = crate::input::keybindings::KeyContext::Normal;
487 self.sync_terminal_to_buffer(self.active_buffer());
488 self.set_status_message(
489 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
490 );
491 return true;
492 }
493 _ => {}
494 }
495 }
496
497 self.send_terminal_key(code, modifiers);
499 true
500 }
501
502 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
511 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
512 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
514 Some(path) => path.clone(),
515 None => return,
516 };
517
518 if let Some(handle) = self.terminal_manager.get(terminal_id) {
521 if let Ok(mut state) = handle.state.lock() {
522 if let Ok(metadata) = self.authority.filesystem.metadata(&backing_file) {
525 state.set_backing_file_history_end(metadata.size);
526 }
527
528 if let Ok(mut file) = self
530 .authority
531 .filesystem
532 .open_file_for_append(&backing_file)
533 {
534 use std::io::BufWriter;
535 let mut writer = BufWriter::new(&mut *file);
536 if let Err(e) = state.append_visible_screen(&mut writer) {
537 tracing::error!(
538 "Failed to append visible screen to backing file: {}",
539 e
540 );
541 }
542 }
543 }
544 }
545
546 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
548 if let Ok(new_state) = EditorState::from_file_with_languages(
549 &backing_file,
550 self.terminal_width,
551 self.terminal_height,
552 large_file_threshold,
553 &self.grammar_registry,
554 &self.config.languages,
555 std::sync::Arc::clone(&self.authority.filesystem),
556 ) {
557 if let Some(state) = self.buffers.get_mut(&buffer_id) {
559 let total_bytes = new_state.buffer.total_bytes();
560 *state = new_state;
561 state.buffer.set_modified(false);
563 if let Some(view_state) = self
565 .split_view_states
566 .get_mut(&self.split_manager.active_split())
567 {
568 view_state.cursors.primary_mut().position = total_bytes;
569 }
570 }
571 }
572
573 if let Some(state) = self.buffers.get_mut(&buffer_id) {
575 state.editing_disabled = true;
576 state.margins.configure_for_line_numbers(false);
577 }
578
579 if let Some(view_state) = self
582 .split_view_states
583 .get_mut(&self.split_manager.active_split())
584 {
585 view_state.viewport.line_wrap_enabled = false;
586
587 view_state.viewport.clear_skip_ensure_visible();
591
592 if let Some(state) = self.buffers.get_mut(&buffer_id) {
594 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
595 }
596 }
597 }
598 }
599
600 pub fn enter_terminal_mode(&mut self) {
606 if self.is_terminal_buffer(self.active_buffer()) {
607 self.terminal_mode = true;
608 self.key_context = crate::input::keybindings::KeyContext::Terminal;
609
610 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
612 state.editing_disabled = false;
613 state.margins.configure_for_line_numbers(false);
614 }
615 if let Some(view_state) = self
616 .split_view_states
617 .get_mut(&self.split_manager.active_split())
618 {
619 view_state.viewport.line_wrap_enabled = false;
620 }
621
622 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
624 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
626 if let Some(handle) = self.terminal_manager.get(terminal_id) {
627 if let Ok(state) = handle.state.lock() {
628 let truncate_pos = state.backing_file_history_end();
629 if let Err(e) = self
632 .authority
633 .filesystem
634 .set_file_length(backing_path, truncate_pos)
635 {
636 tracing::warn!("Failed to truncate terminal backing file: {}", e);
637 }
638 }
639 }
640 }
641
642 if let Some(handle) = self.terminal_manager.get(terminal_id) {
644 if let Ok(mut state) = handle.state.lock() {
645 state.scroll_to_bottom();
646 }
647 }
648 }
649
650 self.resize_visible_terminals();
652
653 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
654 }
655 }
656
657 pub fn get_terminal_content(
659 &self,
660 buffer_id: BufferId,
661 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
662 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
663 let handle = self.terminal_manager.get(*terminal_id)?;
664 let state = handle.state.lock().ok()?;
665
666 let (_, rows) = state.size();
667 let mut content = Vec::with_capacity(rows as usize);
668
669 for row in 0..rows {
670 content.push(state.get_line(row));
671 }
672
673 Some(content)
674 }
675}
676
677impl Editor {
678 pub fn is_terminal_mode(&self) -> bool {
680 self.terminal_mode
681 }
682
683 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
685 self.terminal_mode_resume.contains(&buffer_id)
686 }
687
688 pub fn is_keyboard_capture(&self) -> bool {
690 self.keyboard_capture
691 }
692
693 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
695 self.config_mut().terminal.jump_to_end_on_output = value;
696 }
697
698 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
700 &self.terminal_manager
701 }
702
703 pub fn terminal_backing_files(
705 &self,
706 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
707 &self.terminal_backing_files
708 }
709
710 pub fn active_buffer_id(&self) -> BufferId {
712 self.active_buffer()
713 }
714
715 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
717 self.buffers
718 .get(&buffer_id)
719 .and_then(|state| state.buffer.to_string())
720 }
721
722 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
724 self.split_view_states
726 .values()
727 .find_map(|vs| {
728 if vs.keyed_states.contains_key(&buffer_id) {
729 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
730 } else {
731 None
732 }
733 })
734 .or_else(|| {
735 self.split_view_states
737 .values()
738 .map(|vs| vs.cursors.primary().position)
739 .next()
740 })
741 }
742
743 pub fn render_terminal_splits(
749 &self,
750 frame: &mut ratatui::Frame,
751 split_areas: &[(
752 crate::model::event::LeafId,
753 BufferId,
754 ratatui::layout::Rect,
755 ratatui::layout::Rect,
756 usize,
757 usize,
758 )],
759 ) {
760 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
761 split_areas
762 {
763 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
765 let is_active = *buffer_id == self.active_buffer();
769 if is_active && !self.terminal_mode {
770 continue;
772 }
773 if let Some(handle) = self.terminal_manager.get(terminal_id) {
775 if let Ok(state) = handle.state.lock() {
776 let cursor_pos = state.cursor_position();
777 let cursor_visible =
779 state.cursor_visible() && is_active && self.terminal_mode;
780 let (_, rows) = state.size();
781
782 let mut content = Vec::with_capacity(rows as usize);
784 for row in 0..rows {
785 content.push(state.get_line(row));
786 }
787
788 frame.render_widget(ratatui::widgets::Clear, *content_rect);
790
791 render::render_terminal_content(
793 &content,
794 cursor_pos,
795 cursor_visible,
796 *content_rect,
797 frame.buffer_mut(),
798 self.theme.terminal_fg,
799 self.theme.terminal_bg,
800 );
801 }
802 }
803 }
804 }
805 }
806}
807
808pub mod render {
810 use crate::services::terminal::TerminalCell;
811 use ratatui::buffer::Buffer;
812 use ratatui::layout::Rect;
813 use ratatui::style::{Color, Modifier, Style};
814
815 pub fn render_terminal_content(
817 content: &[Vec<TerminalCell>],
818 cursor_pos: (u16, u16),
819 cursor_visible: bool,
820 area: Rect,
821 buf: &mut Buffer,
822 default_fg: Color,
823 default_bg: Color,
824 ) {
825 for (row_idx, row) in content.iter().enumerate() {
826 if row_idx as u16 >= area.height {
827 break;
828 }
829
830 let y = area.y + row_idx as u16;
831
832 for (col_idx, cell) in row.iter().enumerate() {
833 if col_idx as u16 >= area.width {
834 break;
835 }
836
837 let x = area.x + col_idx as u16;
838
839 let mut style = Style::default().fg(default_fg).bg(default_bg);
841
842 if let Some((r, g, b)) = cell.fg {
844 style = style.fg(Color::Rgb(r, g, b));
845 }
846
847 if let Some((r, g, b)) = cell.bg {
848 style = style.bg(Color::Rgb(r, g, b));
849 }
850
851 if cell.bold {
853 style = style.add_modifier(Modifier::BOLD);
854 }
855 if cell.italic {
856 style = style.add_modifier(Modifier::ITALIC);
857 }
858 if cell.underline {
859 style = style.add_modifier(Modifier::UNDERLINED);
860 }
861 if cell.inverse {
862 style = style.add_modifier(Modifier::REVERSED);
863 }
864
865 if cursor_visible
867 && row_idx as u16 == cursor_pos.1
868 && col_idx as u16 == cursor_pos.0
869 {
870 style = style.add_modifier(Modifier::REVERSED);
871 }
872
873 buf.set_string(x, y, cell.c.to_string(), style);
874 }
875 }
876 }
877}
878
879fn encode_sgr_mouse(
882 col: u16,
883 row: u16,
884 kind: crate::input::handler::TerminalMouseEventKind,
885 modifiers: crossterm::event::KeyModifiers,
886) -> Option<Vec<u8>> {
887 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
888
889 let cx = col + 1;
891 let cy = row + 1;
892
893 let (button_code, is_release) = match kind {
895 TerminalMouseEventKind::Down(btn) => {
896 let code = match btn {
897 TerminalMouseButton::Left => 0,
898 TerminalMouseButton::Middle => 1,
899 TerminalMouseButton::Right => 2,
900 };
901 (code, false)
902 }
903 TerminalMouseEventKind::Up(btn) => {
904 let code = match btn {
905 TerminalMouseButton::Left => 0,
906 TerminalMouseButton::Middle => 1,
907 TerminalMouseButton::Right => 2,
908 };
909 (code, true)
910 }
911 TerminalMouseEventKind::Drag(btn) => {
912 let code = match btn {
913 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
917 (code, false)
918 }
919 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
921 TerminalMouseEventKind::ScrollDown => (65, false),
922 };
923
924 let mut cb = button_code;
926 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
927 cb += 4;
928 }
929 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
930 cb += 8;
931 }
932 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
933 cb += 16;
934 }
935
936 let terminator = if is_release { 'm' } else { 'M' };
938 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
939}
940
941fn encode_x10_mouse(
944 col: u16,
945 row: u16,
946 kind: crate::input::handler::TerminalMouseEventKind,
947 modifiers: crossterm::event::KeyModifiers,
948) -> Option<Vec<u8>> {
949 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
950
951 let cx = (col.min(222) + 1 + 32) as u8;
954 let cy = (row.min(222) + 1 + 32) as u8;
955
956 let button_code: u8 = match kind {
958 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
959 TerminalMouseButton::Left => 0,
960 TerminalMouseButton::Middle => 1,
961 TerminalMouseButton::Right => 2,
962 },
963 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
965 TerminalMouseEventKind::ScrollUp => 64,
966 TerminalMouseEventKind::ScrollDown => 65,
967 };
968
969 let mut cb = button_code;
971 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
972 cb += 32; }
974 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
975 cb += 4;
976 }
977 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
978 cb += 8;
979 }
980 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
981 cb += 16;
982 }
983
984 let cb = cb + 32;
986
987 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
988}