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 if let Some(bytes) = crate::services::terminal::pty::key_to_pty_bytes(code, modifiers) {
322 self.send_terminal_input(&bytes);
323 }
324 }
325
326 pub fn send_terminal_mouse(
328 &mut self,
329 col: u16,
330 row: u16,
331 kind: crate::input::handler::TerminalMouseEventKind,
332 modifiers: crossterm::event::KeyModifiers,
333 ) {
334 use crate::input::handler::TerminalMouseEventKind;
335
336 let use_sgr = self
338 .get_active_terminal_state()
339 .map(|s| s.uses_sgr_mouse())
340 .unwrap_or(true); let uses_alt_scroll = self
344 .get_active_terminal_state()
345 .map(|s| s.uses_alternate_scroll())
346 .unwrap_or(false);
347
348 if uses_alt_scroll {
349 match kind {
350 TerminalMouseEventKind::ScrollUp => {
351 for _ in 0..3 {
353 self.send_terminal_input(b"\x1b[A");
354 }
355 return;
356 }
357 TerminalMouseEventKind::ScrollDown => {
358 for _ in 0..3 {
360 self.send_terminal_input(b"\x1b[B");
361 }
362 return;
363 }
364 _ => {}
365 }
366 }
367
368 let bytes = if use_sgr {
370 encode_sgr_mouse(col, row, kind, modifiers)
371 } else {
372 encode_x10_mouse(col, row, kind, modifiers)
373 };
374
375 if let Some(bytes) = bytes {
376 self.send_terminal_input(&bytes);
377 }
378 }
379
380 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
383 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
384 if let Some(handle) = self.terminal_manager.get(terminal_id) {
385 if let Ok(state) = handle.state.lock() {
386 return state.is_alternate_screen();
387 }
388 }
389 }
390 false
391 }
392
393 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
395 let cols = self.terminal_width.saturating_sub(2).max(40);
398 let rows = self.terminal_height.saturating_sub(4).max(10);
399 (cols, rows)
400 }
401
402 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
404 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
405 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
406 handle.resize(cols, rows);
407 }
408 }
409 }
410
411 pub fn resize_visible_terminals(&mut self) {
414 let file_explorer_width = if self.file_explorer_visible {
416 (self.terminal_width as f32 * self.file_explorer_width_percent) as u16
417 } else {
418 0
419 };
420 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
421 let editor_area = ratatui::layout::Rect::new(
422 file_explorer_width,
423 1, editor_width,
425 self.terminal_height.saturating_sub(2), );
427
428 let visible_buffers = self.split_manager.get_visible_buffers(editor_area);
430
431 for (_split_id, buffer_id, split_area) in visible_buffers {
433 if self.terminal_buffers.contains_key(&buffer_id) {
434 let content_height = split_area.height.saturating_sub(2);
437 let content_width = split_area.width.saturating_sub(2);
438
439 if content_width > 0 && content_height > 0 {
440 self.resize_terminal(buffer_id, content_width, content_height);
441 }
442 }
443 }
444 }
445
446 pub fn handle_terminal_key(
448 &mut self,
449 code: crossterm::event::KeyCode,
450 modifiers: crossterm::event::KeyModifiers,
451 ) -> bool {
452 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
455 match code {
456 crossterm::event::KeyCode::Char(' ')
457 | crossterm::event::KeyCode::Char(']')
458 | crossterm::event::KeyCode::Char('`') => {
459 self.terminal_mode = false;
461 self.key_context = crate::input::keybindings::KeyContext::Normal;
462 self.sync_terminal_to_buffer(self.active_buffer());
463 self.set_status_message(
464 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
465 );
466 return true;
467 }
468 _ => {}
469 }
470 }
471
472 self.send_terminal_key(code, modifiers);
474 true
475 }
476
477 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
486 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
487 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
489 Some(path) => path.clone(),
490 None => return,
491 };
492
493 if let Some(handle) = self.terminal_manager.get(terminal_id) {
496 if let Ok(mut state) = handle.state.lock() {
497 if let Ok(metadata) = self.filesystem.metadata(&backing_file) {
500 state.set_backing_file_history_end(metadata.size);
501 }
502
503 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_file) {
505 use std::io::BufWriter;
506 let mut writer = BufWriter::new(&mut *file);
507 if let Err(e) = state.append_visible_screen(&mut writer) {
508 tracing::error!(
509 "Failed to append visible screen to backing file: {}",
510 e
511 );
512 }
513 }
514 }
515 }
516
517 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
519 if let Ok(new_state) = EditorState::from_file_with_languages(
520 &backing_file,
521 self.terminal_width,
522 self.terminal_height,
523 large_file_threshold,
524 &self.grammar_registry,
525 &self.config.languages,
526 std::sync::Arc::clone(&self.filesystem),
527 ) {
528 if let Some(state) = self.buffers.get_mut(&buffer_id) {
530 let total_bytes = new_state.buffer.total_bytes();
531 *state = new_state;
532 state.buffer.set_modified(false);
534 if let Some(view_state) = self
536 .split_view_states
537 .get_mut(&self.split_manager.active_split())
538 {
539 view_state.cursors.primary_mut().position = total_bytes;
540 }
541 }
542 }
543
544 if let Some(state) = self.buffers.get_mut(&buffer_id) {
546 state.editing_disabled = true;
547 state.margins.configure_for_line_numbers(false);
548 }
549
550 if let Some(view_state) = self
553 .split_view_states
554 .get_mut(&self.split_manager.active_split())
555 {
556 view_state.viewport.line_wrap_enabled = false;
557
558 view_state.viewport.clear_skip_ensure_visible();
562
563 if let Some(state) = self.buffers.get_mut(&buffer_id) {
565 view_state.ensure_cursor_visible(&mut state.buffer, &state.marker_list);
566 }
567 }
568 }
569 }
570
571 pub fn enter_terminal_mode(&mut self) {
577 if self.is_terminal_buffer(self.active_buffer()) {
578 self.terminal_mode = true;
579 self.key_context = crate::input::keybindings::KeyContext::Terminal;
580
581 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
583 state.editing_disabled = false;
584 state.margins.configure_for_line_numbers(false);
585 }
586 if let Some(view_state) = self
587 .split_view_states
588 .get_mut(&self.split_manager.active_split())
589 {
590 view_state.viewport.line_wrap_enabled = false;
591 }
592
593 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
595 if let Some(backing_path) = self.terminal_backing_files.get(&terminal_id) {
597 if let Some(handle) = self.terminal_manager.get(terminal_id) {
598 if let Ok(state) = handle.state.lock() {
599 let truncate_pos = state.backing_file_history_end();
600 if let Err(e) =
603 self.filesystem.set_file_length(backing_path, truncate_pos)
604 {
605 tracing::warn!("Failed to truncate terminal backing file: {}", e);
606 }
607 }
608 }
609 }
610
611 if let Some(handle) = self.terminal_manager.get(terminal_id) {
613 if let Ok(mut state) = handle.state.lock() {
614 state.scroll_to_bottom();
615 }
616 }
617 }
618
619 self.resize_visible_terminals();
621
622 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
623 }
624 }
625
626 pub fn get_terminal_content(
628 &self,
629 buffer_id: BufferId,
630 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
631 let terminal_id = self.terminal_buffers.get(&buffer_id)?;
632 let handle = self.terminal_manager.get(*terminal_id)?;
633 let state = handle.state.lock().ok()?;
634
635 let (_, rows) = state.size();
636 let mut content = Vec::with_capacity(rows as usize);
637
638 for row in 0..rows {
639 content.push(state.get_line(row));
640 }
641
642 Some(content)
643 }
644}
645
646impl Editor {
647 pub fn is_terminal_mode(&self) -> bool {
649 self.terminal_mode
650 }
651
652 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
654 self.terminal_mode_resume.contains(&buffer_id)
655 }
656
657 pub fn is_keyboard_capture(&self) -> bool {
659 self.keyboard_capture
660 }
661
662 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
664 self.config.terminal.jump_to_end_on_output = value;
665 }
666
667 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
669 &self.terminal_manager
670 }
671
672 pub fn terminal_backing_files(
674 &self,
675 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
676 &self.terminal_backing_files
677 }
678
679 pub fn active_buffer_id(&self) -> BufferId {
681 self.active_buffer()
682 }
683
684 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
686 self.buffers
687 .get(&buffer_id)
688 .and_then(|state| state.buffer.to_string())
689 }
690
691 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
693 self.split_view_states
695 .values()
696 .find_map(|vs| {
697 if vs.keyed_states.contains_key(&buffer_id) {
698 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
699 } else {
700 None
701 }
702 })
703 .or_else(|| {
704 self.split_view_states
706 .values()
707 .find_map(|vs| Some(vs.cursors.primary().position))
708 })
709 }
710
711 pub fn render_terminal_splits(
717 &self,
718 frame: &mut ratatui::Frame,
719 split_areas: &[(
720 crate::model::event::LeafId,
721 BufferId,
722 ratatui::layout::Rect,
723 ratatui::layout::Rect,
724 usize,
725 usize,
726 )],
727 ) {
728 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
729 split_areas
730 {
731 if let Some(&terminal_id) = self.terminal_buffers.get(buffer_id) {
733 let is_active = *buffer_id == self.active_buffer();
737 if is_active && !self.terminal_mode {
738 continue;
740 }
741 if let Some(handle) = self.terminal_manager.get(terminal_id) {
743 if let Ok(state) = handle.state.lock() {
744 let cursor_pos = state.cursor_position();
745 let cursor_visible =
747 state.cursor_visible() && is_active && self.terminal_mode;
748 let (_, rows) = state.size();
749
750 let mut content = Vec::with_capacity(rows as usize);
752 for row in 0..rows {
753 content.push(state.get_line(row));
754 }
755
756 frame.render_widget(ratatui::widgets::Clear, *content_rect);
758
759 render::render_terminal_content(
761 &content,
762 cursor_pos,
763 cursor_visible,
764 *content_rect,
765 frame.buffer_mut(),
766 self.theme.terminal_fg,
767 self.theme.terminal_bg,
768 );
769 }
770 }
771 }
772 }
773 }
774}
775
776pub mod render {
778 use crate::services::terminal::TerminalCell;
779 use ratatui::buffer::Buffer;
780 use ratatui::layout::Rect;
781 use ratatui::style::{Color, Modifier, Style};
782
783 pub fn render_terminal_content(
785 content: &[Vec<TerminalCell>],
786 cursor_pos: (u16, u16),
787 cursor_visible: bool,
788 area: Rect,
789 buf: &mut Buffer,
790 default_fg: Color,
791 default_bg: Color,
792 ) {
793 for (row_idx, row) in content.iter().enumerate() {
794 if row_idx as u16 >= area.height {
795 break;
796 }
797
798 let y = area.y + row_idx as u16;
799
800 for (col_idx, cell) in row.iter().enumerate() {
801 if col_idx as u16 >= area.width {
802 break;
803 }
804
805 let x = area.x + col_idx as u16;
806
807 let mut style = Style::default().fg(default_fg).bg(default_bg);
809
810 if let Some((r, g, b)) = cell.fg {
812 style = style.fg(Color::Rgb(r, g, b));
813 }
814
815 if let Some((r, g, b)) = cell.bg {
816 style = style.bg(Color::Rgb(r, g, b));
817 }
818
819 if cell.bold {
821 style = style.add_modifier(Modifier::BOLD);
822 }
823 if cell.italic {
824 style = style.add_modifier(Modifier::ITALIC);
825 }
826 if cell.underline {
827 style = style.add_modifier(Modifier::UNDERLINED);
828 }
829 if cell.inverse {
830 style = style.add_modifier(Modifier::REVERSED);
831 }
832
833 if cursor_visible
835 && row_idx as u16 == cursor_pos.1
836 && col_idx as u16 == cursor_pos.0
837 {
838 style = style.add_modifier(Modifier::REVERSED);
839 }
840
841 buf.set_string(x, y, cell.c.to_string(), style);
842 }
843 }
844 }
845}
846
847fn encode_sgr_mouse(
850 col: u16,
851 row: u16,
852 kind: crate::input::handler::TerminalMouseEventKind,
853 modifiers: crossterm::event::KeyModifiers,
854) -> Option<Vec<u8>> {
855 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
856
857 let cx = col + 1;
859 let cy = row + 1;
860
861 let (button_code, is_release) = match kind {
863 TerminalMouseEventKind::Down(btn) => {
864 let code = match btn {
865 TerminalMouseButton::Left => 0,
866 TerminalMouseButton::Middle => 1,
867 TerminalMouseButton::Right => 2,
868 };
869 (code, false)
870 }
871 TerminalMouseEventKind::Up(btn) => {
872 let code = match btn {
873 TerminalMouseButton::Left => 0,
874 TerminalMouseButton::Middle => 1,
875 TerminalMouseButton::Right => 2,
876 };
877 (code, true)
878 }
879 TerminalMouseEventKind::Drag(btn) => {
880 let code = match btn {
881 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
885 (code, false)
886 }
887 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
889 TerminalMouseEventKind::ScrollDown => (65, false),
890 };
891
892 let mut cb = button_code;
894 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
895 cb += 4;
896 }
897 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
898 cb += 8;
899 }
900 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
901 cb += 16;
902 }
903
904 let terminator = if is_release { 'm' } else { 'M' };
906 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
907}
908
909fn encode_x10_mouse(
912 col: u16,
913 row: u16,
914 kind: crate::input::handler::TerminalMouseEventKind,
915 modifiers: crossterm::event::KeyModifiers,
916) -> Option<Vec<u8>> {
917 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
918
919 let cx = (col.min(222) + 1 + 32) as u8;
922 let cy = (row.min(222) + 1 + 32) as u8;
923
924 let button_code: u8 = match kind {
926 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
927 TerminalMouseButton::Left => 0,
928 TerminalMouseButton::Middle => 1,
929 TerminalMouseButton::Right => 2,
930 },
931 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
933 TerminalMouseEventKind::ScrollUp => 64,
934 TerminalMouseEventKind::ScrollDown => 65,
935 };
936
937 let mut cb = button_code;
939 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
940 cb += 32; }
942 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
943 cb += 4;
944 }
945 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
946 cb += 8;
947 }
948 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
949 cb += 16;
950 }
951
952 let cb = cb + 32;
954
955 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
956}