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(crate) fn spawn_terminal_session(&mut self) -> Option<TerminalId> {
61 let (cols, rows) = self.get_terminal_dimensions();
67
68 if let Some(ref bridge) = self.async_bridge {
70 self.terminal_manager.set_async_bridge(bridge.clone());
71 }
72
73 let terminal_root = self.dir_context.terminal_dir_for(&self.working_dir);
75 if let Err(e) = self.authority.filesystem.create_dir_all(&terminal_root) {
76 tracing::warn!("Failed to create terminal directory: {}", e);
77 }
78 let predicted_terminal_id = self.terminal_manager.next_terminal_id();
80 let log_path =
81 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
82 let backing_path =
83 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
84 self.terminal_backing_files
86 .insert(predicted_terminal_id, backing_path);
87
88 let backing_path_for_spawn = self
90 .terminal_backing_files
91 .get(&predicted_terminal_id)
92 .cloned();
93 match self.terminal_manager.spawn(
94 cols,
95 rows,
96 Some(self.working_dir.clone()),
97 Some(log_path.clone()),
98 backing_path_for_spawn,
99 self.resolved_terminal_wrapper(),
100 ) {
101 Ok(terminal_id) => {
102 self.terminal_log_files
104 .insert(terminal_id, log_path.clone());
105 if terminal_id != predicted_terminal_id {
107 self.terminal_backing_files.remove(&predicted_terminal_id);
108 let backing_path =
109 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
110 self.terminal_backing_files
111 .insert(terminal_id, backing_path);
112 }
113 Some(terminal_id)
114 }
115 Err(e) => {
116 self.set_status_message(
117 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
118 );
119 tracing::error!("Failed to open terminal: {}", e);
120 None
121 }
122 }
123 }
124
125 pub fn open_terminal(&mut self) {
127 let Some(terminal_id) = self.spawn_terminal_session() else {
128 return;
129 };
130
131 let buffer_id =
133 self.create_terminal_buffer_attached(terminal_id, self.split_manager.active_split());
134
135 self.set_active_buffer(buffer_id);
137
138 self.terminal_mode = true;
140 self.key_context = crate::input::keybindings::KeyContext::Terminal;
141
142 self.resize_visible_terminals();
144
145 let exit_key = self
147 .keybindings
148 .read()
149 .unwrap()
150 .find_keybinding_for_action(
151 "terminal_escape",
152 crate::input::keybindings::KeyContext::Terminal,
153 )
154 .unwrap_or_else(|| "Ctrl+Space".to_string());
155 self.set_status_message(
156 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
157 );
158 tracing::info!(
159 "Opened terminal {:?} with buffer {:?}",
160 terminal_id,
161 buffer_id
162 );
163 }
164
165 pub(crate) fn create_terminal_buffer_attached(
167 &mut self,
168 terminal_id: TerminalId,
169 split_id: crate::model::event::LeafId,
170 ) -> BufferId {
171 let buffer_id = BufferId(self.next_buffer_id);
172 self.next_buffer_id += 1;
173
174 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
176
177 let backing_file = self
179 .terminal_backing_files
180 .get(&terminal_id)
181 .cloned()
182 .unwrap_or_else(|| {
183 let root = self.dir_context.terminal_dir_for(&self.working_dir);
184 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
185 tracing::warn!("Failed to create terminal directory: {}", e);
186 }
187 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
188 });
189
190 if !self.authority.filesystem.exists(&backing_file) {
193 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
194 tracing::warn!("Failed to create terminal backing file: {}", e);
195 }
196 }
197
198 self.terminal_backing_files
200 .insert(terminal_id, backing_file.clone());
201
202 let mut state = EditorState::new_with_path(
204 large_file_threshold,
205 std::sync::Arc::clone(&self.authority.filesystem),
206 backing_file.clone(),
207 );
208 state.margins.configure_for_line_numbers(false);
210 self.buffers.insert(buffer_id, state);
211
212 let metadata = BufferMetadata::virtual_buffer(
215 format!("*Terminal {}*", terminal_id.0),
216 "terminal".into(),
217 false,
218 );
219 self.buffer_metadata.insert(buffer_id, metadata);
220
221 self.terminal_buffers.insert(buffer_id, terminal_id);
223
224 self.event_logs
226 .insert(buffer_id, crate::model::event::EventLog::new());
227
228 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
230 view_state.add_buffer(buffer_id);
231 view_state.viewport.line_wrap_enabled = false;
233 }
234
235 buffer_id
236 }
237
238 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
240 let buffer_id = BufferId(self.next_buffer_id);
241 self.next_buffer_id += 1;
242
243 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
245
246 let backing_file = self
247 .terminal_backing_files
248 .get(&terminal_id)
249 .cloned()
250 .unwrap_or_else(|| {
251 let root = self.dir_context.terminal_dir_for(&self.working_dir);
252 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
253 tracing::warn!("Failed to create terminal directory: {}", e);
254 }
255 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
256 });
257
258 if !self.authority.filesystem.exists(&backing_file) {
260 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
261 tracing::warn!("Failed to create terminal backing file: {}", e);
262 }
263 }
264
265 let mut state = EditorState::new_with_path(
267 large_file_threshold,
268 std::sync::Arc::clone(&self.authority.filesystem),
269 backing_file.clone(),
270 );
271 state.margins.configure_for_line_numbers(false);
272 self.buffers.insert(buffer_id, state);
273
274 let metadata = BufferMetadata::virtual_buffer(
275 format!("*Terminal {}*", terminal_id.0),
276 "terminal".into(),
277 false,
278 );
279 self.buffer_metadata.insert(buffer_id, metadata);
280 self.terminal_buffers.insert(buffer_id, terminal_id);
281 self.event_logs
282 .insert(buffer_id, crate::model::event::EventLog::new());
283
284 buffer_id
285 }
286
287 pub fn close_terminal(&mut self) {
289 let buffer_id = self.active_buffer();
290
291 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
292 self.terminal_manager.close(terminal_id);
294 self.terminal_buffers.remove(&buffer_id);
295 self.ephemeral_terminals.remove(&terminal_id);
296
297 let backing_file = self.terminal_backing_files.remove(&terminal_id);
299 if let Some(ref path) = backing_file {
300 #[allow(clippy::let_underscore_must_use)]
302 let _ = self.authority.filesystem.remove_file(path);
303 }
304 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
306 if backing_file.as_ref() != Some(&log_file) {
307 #[allow(clippy::let_underscore_must_use)]
309 let _ = self.authority.filesystem.remove_file(&log_file);
310 }
311 }
312
313 self.terminal_mode = false;
315 self.key_context = crate::input::keybindings::KeyContext::Normal;
316
317 if let Err(e) = self.close_buffer(buffer_id) {
319 tracing::warn!("Failed to close terminal buffer: {}", e);
320 }
321
322 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
323 } else {
324 self.set_status_message(t!("status.not_viewing_terminal").to_string());
325 }
326 }
327
328 pub fn is_terminal_buffer(&self, buffer_id: BufferId) -> bool {
330 self.terminal_buffers.contains_key(&buffer_id)
331 }
332
333 pub fn get_terminal_id(&self, buffer_id: BufferId) -> Option<TerminalId> {
335 self.terminal_buffers.get(&buffer_id).copied()
336 }
337
338 pub fn get_active_terminal_state(
340 &self,
341 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
342 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
343 let handle = self.terminal_manager.get(*terminal_id)?;
344 handle.state.lock().ok()
345 }
346
347 pub fn send_terminal_input(&mut self, data: &[u8]) {
349 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
350 if let Some(handle) = self.terminal_manager.get(terminal_id) {
351 handle.write(data);
352 }
353 }
354 }
355
356 pub fn send_terminal_key(
358 &mut self,
359 code: crossterm::event::KeyCode,
360 modifiers: crossterm::event::KeyModifiers,
361 ) {
362 let app_cursor = self
363 .get_active_terminal_state()
364 .map(|s| s.is_app_cursor())
365 .unwrap_or(false);
366 if let Some(bytes) =
367 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
368 {
369 self.send_terminal_input(&bytes);
370 }
371 }
372
373 pub fn send_terminal_mouse(
375 &mut self,
376 col: u16,
377 row: u16,
378 kind: crate::input::handler::TerminalMouseEventKind,
379 modifiers: crossterm::event::KeyModifiers,
380 ) {
381 use crate::input::handler::TerminalMouseEventKind;
382
383 let use_sgr = self
385 .get_active_terminal_state()
386 .map(|s| s.uses_sgr_mouse())
387 .unwrap_or(true); let uses_alt_scroll = self
391 .get_active_terminal_state()
392 .map(|s| s.uses_alternate_scroll())
393 .unwrap_or(false);
394
395 if uses_alt_scroll {
396 match kind {
397 TerminalMouseEventKind::ScrollUp => {
398 for _ in 0..3 {
400 self.send_terminal_input(b"\x1b[A");
401 }
402 return;
403 }
404 TerminalMouseEventKind::ScrollDown => {
405 for _ in 0..3 {
407 self.send_terminal_input(b"\x1b[B");
408 }
409 return;
410 }
411 _ => {}
412 }
413 }
414
415 let bytes = if use_sgr {
417 encode_sgr_mouse(col, row, kind, modifiers)
418 } else {
419 encode_x10_mouse(col, row, kind, modifiers)
420 };
421
422 if let Some(bytes) = bytes {
423 self.send_terminal_input(&bytes);
424 }
425 }
426
427 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
430 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
431 if let Some(handle) = self.terminal_manager.get(terminal_id) {
432 if let Ok(state) = handle.state.lock() {
433 return state.is_alternate_screen();
434 }
435 }
436 }
437 false
438 }
439
440 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
442 let cols = self.terminal_width.saturating_sub(2).max(40);
445 let rows = self.terminal_height.saturating_sub(4).max(10);
446 (cols, rows)
447 }
448
449 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
451 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
452 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
453 handle.resize(cols, rows);
454 }
455 }
456 }
457
458 pub fn resize_visible_terminals(&mut self) {
461 let file_explorer_width = if self.file_explorer_visible {
463 self.file_explorer_width.to_cols(self.terminal_width)
464 } else {
465 0
466 };
467 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
468 let editor_area = ratatui::layout::Rect::new(
469 file_explorer_width,
470 1, editor_width,
472 self.terminal_height.saturating_sub(2), );
474
475 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
477
478 for (_split_id, buffer_id, split_area) in visible_buffers {
480 if self.terminal_buffers.contains_key(&buffer_id) {
481 let content_height = split_area.height.saturating_sub(2);
484 let content_width = split_area.width.saturating_sub(2);
485
486 if content_width > 0 && content_height > 0 {
487 self.resize_terminal(buffer_id, content_width, content_height);
488 }
489 }
490 }
491 }
492
493 pub fn handle_terminal_key(
495 &mut self,
496 code: crossterm::event::KeyCode,
497 modifiers: crossterm::event::KeyModifiers,
498 ) -> bool {
499 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
502 match code {
503 crossterm::event::KeyCode::Char(' ')
504 | crossterm::event::KeyCode::Char(']')
505 | crossterm::event::KeyCode::Char('`') => {
506 self.terminal_mode = false;
508 self.key_context = crate::input::keybindings::KeyContext::Normal;
509 self.sync_terminal_to_buffer(self.active_buffer());
510 self.set_status_message(
511 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
512 );
513 return true;
514 }
515 _ => {}
516 }
517 }
518
519 self.send_terminal_key(code, modifiers);
521 true
522 }
523
524 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
533 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
534 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
536 Some(path) => path.clone(),
537 None => return,
538 };
539
540 if let Some(handle) = self.terminal_manager.get(terminal_id) {
543 if let Ok(mut state) = handle.state.lock() {
544 if let Ok(metadata) = self.authority.filesystem.metadata(&backing_file) {
547 state.set_backing_file_history_end(metadata.size);
548 }
549
550 if let Ok(mut file) = self
552 .authority
553 .filesystem
554 .open_file_for_append(&backing_file)
555 {
556 use std::io::BufWriter;
557 let mut writer = BufWriter::new(&mut *file);
558 if let Err(e) = state.append_visible_screen(&mut writer) {
559 tracing::error!(
560 "Failed to append visible screen to backing file: {}",
561 e
562 );
563 }
564 }
565 }
566 }
567
568 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
570 if let Ok(new_state) = EditorState::from_file_with_languages(
571 &backing_file,
572 self.terminal_width,
573 self.terminal_height,
574 large_file_threshold,
575 &self.grammar_registry,
576 &self.config.languages,
577 std::sync::Arc::clone(&self.authority.filesystem),
578 ) {
579 if let Some(state) = self.buffers.get_mut(&buffer_id) {
581 let total_bytes = new_state.buffer.total_bytes();
582 *state = new_state;
583 state.buffer.set_modified(false);
585 if let Some(view_state) = self
587 .split_view_states
588 .get_mut(&self.split_manager.active_split())
589 {
590 view_state.cursors.primary_mut().position = total_bytes;
591 }
592 }
593 }
594
595 if let Some(state) = self.buffers.get_mut(&buffer_id) {
597 state.editing_disabled = true;
598 state.margins.configure_for_line_numbers(false);
599 }
600
601 if let Some(view_state) = self
604 .split_view_states
605 .get_mut(&self.split_manager.active_split())
606 {
607 view_state.viewport.line_wrap_enabled = false;
608
609 view_state.viewport.clear_skip_ensure_visible();
613
614 if let Some(state) = self.buffers.get_mut(&buffer_id) {
616 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
617 }
618 }
619 }
620 }
621
622 pub fn enter_terminal_mode(&mut self) {
628 if self.is_terminal_buffer(self.active_buffer()) {
629 self.terminal_mode = true;
630 self.key_context = crate::input::keybindings::KeyContext::Terminal;
631
632 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
634 state.editing_disabled = false;
635 state.margins.configure_for_line_numbers(false);
636 }
637 if let Some(view_state) = self
638 .split_view_states
639 .get_mut(&self.split_manager.active_split())
640 {
641 view_state.viewport.line_wrap_enabled = false;
642 }
643
644 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
646 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
648 if let Some(handle) = self.terminal_manager.get(terminal_id) {
649 if let Ok(state) = handle.state.lock() {
650 let truncate_pos = state.backing_file_history_end();
651 if let Err(e) = self
654 .authority
655 .filesystem
656 .set_file_length(backing_path, truncate_pos)
657 {
658 tracing::warn!("Failed to truncate terminal backing file: {}", e);
659 }
660 }
661 }
662 }
663
664 if let Some(handle) = self.terminal_manager.get(terminal_id) {
666 if let Ok(mut state) = handle.state.lock() {
667 state.scroll_to_bottom();
668 }
669 }
670 }
671
672 self.resize_visible_terminals();
674
675 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
676 }
677 }
678
679 pub fn get_terminal_content(
681 &self,
682 buffer_id: BufferId,
683 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
684 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
685 let handle = self.terminal_manager.get(*terminal_id)?;
686 let state = handle.state.lock().ok()?;
687
688 let (_, rows) = state.size();
689 let mut content = Vec::with_capacity(rows as usize);
690
691 for row in 0..rows {
692 content.push(state.get_line(row));
693 }
694
695 Some(content)
696 }
697}
698
699impl Editor {
700 pub fn is_terminal_mode(&self) -> bool {
702 self.terminal_mode
703 }
704
705 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
707 self.terminal_mode_resume.contains(&buffer_id)
708 }
709
710 pub fn is_keyboard_capture(&self) -> bool {
712 self.keyboard_capture
713 }
714
715 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
717 self.config_mut().terminal.jump_to_end_on_output = value;
718 }
719
720 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
722 &self.terminal_manager
723 }
724
725 pub fn terminal_backing_files(
727 &self,
728 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
729 &self.terminal_backing_files
730 }
731
732 pub fn active_buffer_id(&self) -> BufferId {
734 self.active_buffer()
735 }
736
737 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
739 self.buffers
740 .get(&buffer_id)
741 .and_then(|state| state.buffer.to_string())
742 }
743
744 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
746 self.split_view_states
748 .values()
749 .find_map(|vs| {
750 if vs.keyed_states.contains_key(&buffer_id) {
751 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
752 } else {
753 None
754 }
755 })
756 .or_else(|| {
757 self.split_view_states
759 .values()
760 .map(|vs| vs.cursors.primary().position)
761 .next()
762 })
763 }
764
765 pub fn render_terminal_splits(
771 &self,
772 frame: &mut ratatui::Frame,
773 split_areas: &[(
774 crate::model::event::LeafId,
775 BufferId,
776 ratatui::layout::Rect,
777 ratatui::layout::Rect,
778 usize,
779 usize,
780 )],
781 ) {
782 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
783 split_areas
784 {
785 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
787 let is_active = *buffer_id == self.active_buffer();
791 if is_active && !self.terminal_mode {
792 continue;
794 }
795 if let Some(handle) = self.terminal_manager.get(terminal_id) {
797 if let Ok(state) = handle.state.lock() {
798 let cursor_pos = state.cursor_position();
799 let cursor_visible =
801 state.cursor_visible() && is_active && self.terminal_mode;
802 let (_, rows) = state.size();
803
804 let mut content = Vec::with_capacity(rows as usize);
806 for row in 0..rows {
807 content.push(state.get_line(row));
808 }
809
810 frame.render_widget(ratatui::widgets::Clear, *content_rect);
812
813 render::render_terminal_content(
815 &content,
816 cursor_pos,
817 cursor_visible,
818 *content_rect,
819 frame.buffer_mut(),
820 self.theme.terminal_fg,
821 self.theme.terminal_bg,
822 );
823 }
824 }
825 }
826 }
827 }
828}
829
830pub mod render {
832 use crate::services::terminal::TerminalCell;
833 use ratatui::buffer::Buffer;
834 use ratatui::layout::Rect;
835 use ratatui::style::{Color, Modifier, Style};
836
837 pub fn render_terminal_content(
839 content: &[Vec<TerminalCell>],
840 cursor_pos: (u16, u16),
841 cursor_visible: bool,
842 area: Rect,
843 buf: &mut Buffer,
844 default_fg: Color,
845 default_bg: Color,
846 ) {
847 for (row_idx, row) in content.iter().enumerate() {
848 if row_idx as u16 >= area.height {
849 break;
850 }
851
852 let y = area.y + row_idx as u16;
853
854 for (col_idx, cell) in row.iter().enumerate() {
855 if col_idx as u16 >= area.width {
856 break;
857 }
858
859 let x = area.x + col_idx as u16;
860
861 let mut style = Style::default().fg(default_fg).bg(default_bg);
863
864 if let Some((r, g, b)) = cell.fg {
866 style = style.fg(Color::Rgb(r, g, b));
867 }
868
869 if let Some((r, g, b)) = cell.bg {
870 style = style.bg(Color::Rgb(r, g, b));
871 }
872
873 if cell.bold {
875 style = style.add_modifier(Modifier::BOLD);
876 }
877 if cell.italic {
878 style = style.add_modifier(Modifier::ITALIC);
879 }
880 if cell.underline {
881 style = style.add_modifier(Modifier::UNDERLINED);
882 }
883 if cell.inverse {
884 style = style.add_modifier(Modifier::REVERSED);
885 }
886
887 if cursor_visible
889 && row_idx as u16 == cursor_pos.1
890 && col_idx as u16 == cursor_pos.0
891 {
892 style = style.add_modifier(Modifier::REVERSED);
893 }
894
895 buf.set_string(x, y, cell.c.to_string(), style);
896 }
897 }
898 }
899}
900
901fn encode_sgr_mouse(
904 col: u16,
905 row: u16,
906 kind: crate::input::handler::TerminalMouseEventKind,
907 modifiers: crossterm::event::KeyModifiers,
908) -> Option<Vec<u8>> {
909 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
910
911 let cx = col + 1;
913 let cy = row + 1;
914
915 let (button_code, is_release) = match kind {
917 TerminalMouseEventKind::Down(btn) => {
918 let code = match btn {
919 TerminalMouseButton::Left => 0,
920 TerminalMouseButton::Middle => 1,
921 TerminalMouseButton::Right => 2,
922 };
923 (code, false)
924 }
925 TerminalMouseEventKind::Up(btn) => {
926 let code = match btn {
927 TerminalMouseButton::Left => 0,
928 TerminalMouseButton::Middle => 1,
929 TerminalMouseButton::Right => 2,
930 };
931 (code, true)
932 }
933 TerminalMouseEventKind::Drag(btn) => {
934 let code = match btn {
935 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
939 (code, false)
940 }
941 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
943 TerminalMouseEventKind::ScrollDown => (65, false),
944 };
945
946 let mut cb = button_code;
948 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
949 cb += 4;
950 }
951 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
952 cb += 8;
953 }
954 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
955 cb += 16;
956 }
957
958 let terminator = if is_release { 'm' } else { 'M' };
960 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
961}
962
963fn encode_x10_mouse(
966 col: u16,
967 row: u16,
968 kind: crate::input::handler::TerminalMouseEventKind,
969 modifiers: crossterm::event::KeyModifiers,
970) -> Option<Vec<u8>> {
971 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
972
973 let cx = (col.min(222) + 1 + 32) as u8;
976 let cy = (row.min(222) + 1 + 32) as u8;
977
978 let button_code: u8 = match kind {
980 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
981 TerminalMouseButton::Left => 0,
982 TerminalMouseButton::Middle => 1,
983 TerminalMouseButton::Right => 2,
984 },
985 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
987 TerminalMouseEventKind::ScrollUp => 64,
988 TerminalMouseEventKind::ScrollDown => 65,
989 };
990
991 let mut cb = button_code;
993 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
994 cb += 32; }
996 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
997 cb += 4;
998 }
999 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
1000 cb += 8;
1001 }
1002 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
1003 cb += 16;
1004 }
1005
1006 let cb = cb + 32;
1008
1009 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
1010}