1use super::window::Window;
27use super::{BufferId, BufferMetadata, Editor};
28use crate::services::authority::TerminalWrapper;
29use crate::services::terminal::TerminalId;
30use crate::state::EditorState;
31use rust_i18n::t;
32
33impl Editor {
34 pub(crate) fn resolved_terminal_wrapper(&self) -> TerminalWrapper {
42 self.authority
43 .terminal_wrapper
44 .clone()
45 .with_user_shell_override(self.config.terminal.shell.as_ref())
46 }
47
48 pub(crate) fn spawn_terminal_session(&mut self) -> Option<TerminalId> {
62 let (cols, rows) = self.get_terminal_dimensions();
68
69 let __window_bridge = self.active_window().bridge.clone();
74 self.active_window_mut()
75 .terminal_manager
76 .set_async_bridge(__window_bridge);
77
78 let terminal_root = self.dir_context.terminal_dir_for(&self.working_dir);
80 if let Err(e) = self.authority.filesystem.create_dir_all(&terminal_root) {
81 tracing::warn!("Failed to create terminal directory: {}", e);
82 }
83 let predicted_terminal_id = self.active_window().terminal_manager.next_terminal_id();
85 let log_path =
86 terminal_root.join(format!("fresh-terminal-{}.log", predicted_terminal_id.0));
87 let backing_path =
88 terminal_root.join(format!("fresh-terminal-{}.txt", predicted_terminal_id.0));
89 self.active_window_mut()
91 .terminal_backing_files
92 .insert(predicted_terminal_id, backing_path);
93
94 let backing_path_for_spawn = self
98 .windows
99 .get(&self.active_window)
100 .map(|w| &w.terminal_backing_files)
101 .expect("active window present")
102 .get(&predicted_terminal_id)
103 .cloned();
104 let working_dir_for_spawn = self.working_dir.clone();
105 let wrapper_for_spawn = self.resolved_terminal_wrapper();
106 match self
107 .windows
108 .get_mut(&self.active_window)
109 .map(|w| &mut w.terminal_manager)
110 .expect("active window present")
111 .spawn(
112 cols,
113 rows,
114 Some(working_dir_for_spawn),
115 Some(log_path.clone()),
116 backing_path_for_spawn,
117 wrapper_for_spawn,
118 ) {
119 Ok(terminal_id) => {
120 self.active_window_mut()
122 .terminal_log_files
123 .insert(terminal_id, log_path.clone());
124 if terminal_id != predicted_terminal_id {
126 self.active_window_mut()
127 .terminal_backing_files
128 .remove(&predicted_terminal_id);
129 let backing_path =
130 terminal_root.join(format!("fresh-terminal-{}.txt", terminal_id.0));
131 self.active_window_mut()
132 .terminal_backing_files
133 .insert(terminal_id, backing_path);
134 }
135 Some(terminal_id)
136 }
137 Err(e) => {
138 self.set_status_message(
139 t!("terminal.failed_to_open", error = e.to_string()).to_string(),
140 );
141 tracing::error!("Failed to open terminal: {}", e);
142 None
143 }
144 }
145 }
146
147 pub fn open_terminal(&mut self) {
149 let Some(terminal_id) = self.spawn_terminal_session() else {
150 return;
151 };
152
153 let buffer_id = self.create_terminal_buffer_attached(
155 terminal_id,
156 self.windows
157 .get(&self.active_window)
158 .and_then(|w| w.buffers.splits())
159 .map(|(mgr, _)| mgr)
160 .expect("active window must have a populated split layout")
161 .active_split(),
162 );
163
164 self.set_active_buffer(buffer_id);
166
167 self.active_window_mut().terminal_mode = true;
169 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
170
171 self.active_window_mut().resize_visible_terminals();
173
174 let exit_key = self
176 .keybindings
177 .read()
178 .unwrap()
179 .find_keybinding_for_action(
180 "terminal_escape",
181 crate::input::keybindings::KeyContext::Terminal,
182 )
183 .unwrap_or_else(|| "Ctrl+Space".to_string());
184 self.set_status_message(
185 t!("terminal.opened", id = terminal_id.0, exit_key = exit_key).to_string(),
186 );
187 tracing::info!(
188 "Opened terminal {:?} with buffer {:?}",
189 terminal_id,
190 buffer_id
191 );
192 }
193
194 pub(crate) fn create_terminal_buffer_attached(
196 &mut self,
197 terminal_id: TerminalId,
198 split_id: crate::model::event::LeafId,
199 ) -> BufferId {
200 let buffer_id = self.alloc_buffer_id();
201
202 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
204
205 let backing_file = self
207 .active_window()
208 .terminal_backing_files
209 .get(&terminal_id)
210 .cloned()
211 .unwrap_or_else(|| {
212 let root = self.dir_context.terminal_dir_for(&self.working_dir);
213 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
214 tracing::warn!("Failed to create terminal directory: {}", e);
215 }
216 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
217 });
218
219 if !self.authority.filesystem.exists(&backing_file) {
222 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
223 tracing::warn!("Failed to create terminal backing file: {}", e);
224 }
225 }
226
227 self.active_window_mut()
229 .terminal_backing_files
230 .insert(terminal_id, backing_file.clone());
231
232 let mut state = EditorState::new_with_path(
234 large_file_threshold,
235 std::sync::Arc::clone(&self.authority.filesystem),
236 backing_file.clone(),
237 );
238 state.margins.configure_for_line_numbers(false);
240 self.windows
241 .get_mut(&self.active_window)
242 .map(|w| &mut w.buffers)
243 .expect("active window present")
244 .insert(buffer_id, state);
245 let metadata = BufferMetadata::virtual_buffer(
248 format!("*Terminal {}*", terminal_id.0),
249 "terminal".into(),
250 false,
251 );
252 self.active_window_mut()
253 .buffer_metadata
254 .insert(buffer_id, metadata);
255
256 self.active_window_mut()
258 .terminal_buffers
259 .insert(buffer_id, terminal_id);
260
261 self.active_window_mut()
263 .event_logs
264 .insert(buffer_id, crate::model::event::EventLog::new());
265
266 if let Some(view_state) = self
268 .windows
269 .get_mut(&self.active_window)
270 .and_then(|w| w.split_view_states_mut())
271 .expect("active window must have a populated split layout")
272 .get_mut(&split_id)
273 {
274 view_state.add_buffer(buffer_id);
275 view_state.viewport.line_wrap_enabled = false;
277 }
278
279 buffer_id
280 }
281
282 pub(crate) fn create_terminal_buffer_detached(&mut self, terminal_id: TerminalId) -> BufferId {
284 let buffer_id = self.alloc_buffer_id();
285
286 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
288
289 let backing_file = self
290 .active_window()
291 .terminal_backing_files
292 .get(&terminal_id)
293 .cloned()
294 .unwrap_or_else(|| {
295 let root = self.dir_context.terminal_dir_for(&self.working_dir);
296 if let Err(e) = self.authority.filesystem.create_dir_all(&root) {
297 tracing::warn!("Failed to create terminal directory: {}", e);
298 }
299 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
300 });
301
302 if !self.authority.filesystem.exists(&backing_file) {
304 if let Err(e) = self.authority.filesystem.write_file(&backing_file, &[]) {
305 tracing::warn!("Failed to create terminal backing file: {}", e);
306 }
307 }
308
309 let mut state = EditorState::new_with_path(
311 large_file_threshold,
312 std::sync::Arc::clone(&self.authority.filesystem),
313 backing_file.clone(),
314 );
315 state.margins.configure_for_line_numbers(false);
316 self.windows
317 .get_mut(&self.active_window)
318 .map(|w| &mut w.buffers)
319 .expect("active window present")
320 .insert(buffer_id, state);
321 let metadata = BufferMetadata::virtual_buffer(
322 format!("*Terminal {}*", terminal_id.0),
323 "terminal".into(),
324 false,
325 );
326 self.active_window_mut()
327 .buffer_metadata
328 .insert(buffer_id, metadata);
329 self.active_window_mut()
330 .terminal_buffers
331 .insert(buffer_id, terminal_id);
332 self.active_window_mut()
333 .event_logs
334 .insert(buffer_id, crate::model::event::EventLog::new());
335
336 buffer_id
337 }
338
339 pub fn close_terminal(&mut self) {
341 let buffer_id = self.active_buffer();
342
343 if let Some(&terminal_id) = self.active_window().terminal_buffers.get(&buffer_id) {
344 self.active_window_mut().terminal_manager.close(terminal_id);
346 self.active_window_mut().terminal_buffers.remove(&buffer_id);
347 self.active_window_mut()
348 .ephemeral_terminals
349 .remove(&terminal_id);
350
351 let backing_file = self
353 .active_window_mut()
354 .terminal_backing_files
355 .remove(&terminal_id);
356 if let Some(ref path) = backing_file {
357 #[allow(clippy::let_underscore_must_use)]
359 let _ = self.authority.filesystem.remove_file(path);
360 }
361 if let Some(log_file) = self
363 .active_window_mut()
364 .terminal_log_files
365 .remove(&terminal_id)
366 {
367 if backing_file.as_ref() != Some(&log_file) {
368 #[allow(clippy::let_underscore_must_use)]
370 let _ = self.authority.filesystem.remove_file(&log_file);
371 }
372 }
373
374 self.active_window_mut().terminal_mode = false;
376 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
377
378 if let Err(e) = self.close_buffer(buffer_id) {
380 tracing::warn!("Failed to close terminal buffer: {}", e);
381 }
382
383 self.set_status_message(t!("terminal.closed", id = terminal_id.0).to_string());
384 } else {
385 self.set_status_message(t!("status.not_viewing_terminal").to_string());
386 }
387 }
388
389 pub(crate) fn get_terminal_dimensions(&self) -> (u16, u16) {
402 let cols = self.terminal_width.saturating_sub(2).max(40);
405 let rows = self.terminal_height.saturating_sub(4).max(10);
406 (cols, rows)
407 }
408
409 pub fn handle_terminal_key(
411 &mut self,
412 code: crossterm::event::KeyCode,
413 modifiers: crossterm::event::KeyModifiers,
414 ) -> bool {
415 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
418 match code {
419 crossterm::event::KeyCode::Char(' ')
420 | crossterm::event::KeyCode::Char(']')
421 | crossterm::event::KeyCode::Char('`') => {
422 self.active_window_mut().terminal_mode = false;
424 self.active_window_mut().key_context =
425 crate::input::keybindings::KeyContext::Normal;
426 {
427 let __b = self.active_buffer();
428 self.active_window_mut().sync_terminal_to_buffer(__b);
429 };
430 self.set_status_message(
431 "Terminal mode disabled - read only (Ctrl+Space to resume)".to_string(),
432 );
433 return true;
434 }
435 _ => {}
436 }
437 }
438
439 self.active_window_mut().send_terminal_key(code, modifiers);
441 true
442 }
443
444 pub fn enter_terminal_mode(&mut self) {
450 if self
451 .active_window()
452 .is_terminal_buffer(self.active_buffer())
453 {
454 self.active_window_mut().terminal_mode = true;
455 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
456
457 let __buffer_id = self.active_buffer();
459 if let Some(state) = self
460 .windows
461 .get_mut(&self.active_window)
462 .map(|w| &mut w.buffers)
463 .expect("active window present")
464 .get_mut(&__buffer_id)
465 {
466 state.editing_disabled = false;
467 state.margins.configure_for_line_numbers(false);
468 }
469 let __active_split = self.split_manager().active_split();
470 if let Some(view_state) = self.split_view_states_mut().get_mut(&__active_split) {
471 view_state.viewport.line_wrap_enabled = false;
472 }
473
474 if let Some(&terminal_id) = self
476 .active_window()
477 .terminal_buffers
478 .get(&self.active_buffer())
479 {
480 if let Some(backing_path) = self
482 .active_window()
483 .terminal_backing_files
484 .get(&terminal_id)
485 {
486 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
487 if let Ok(state) = handle.state.lock() {
488 let truncate_pos = state.backing_file_history_end();
489 if let Err(e) = self
492 .authority
493 .filesystem
494 .set_file_length(backing_path, truncate_pos)
495 {
496 tracing::warn!("Failed to truncate terminal backing file: {}", e);
497 }
498 }
499 }
500 }
501
502 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
504 if let Ok(mut state) = handle.state.lock() {
505 state.scroll_to_bottom();
506 }
507 }
508 }
509
510 self.active_window_mut().resize_visible_terminals();
512
513 self.set_status_message(t!("status.terminal_mode_enabled").to_string());
514 }
515 }
516
517 pub fn get_terminal_content(
519 &self,
520 buffer_id: BufferId,
521 ) -> Option<Vec<Vec<crate::services::terminal::TerminalCell>>> {
522 let terminal_id = self.active_window().terminal_buffers.get(&buffer_id)?;
523 let handle = self.active_window().terminal_manager.get(*terminal_id)?;
524 let state = handle.state.lock().ok()?;
525
526 let (_, rows) = state.size();
527 let mut content = Vec::with_capacity(rows as usize);
528
529 for row in 0..rows {
530 content.push(state.get_line(row));
531 }
532
533 Some(content)
534 }
535}
536
537impl Window {
538 pub fn get_active_terminal_state(
540 &self,
541 ) -> Option<std::sync::MutexGuard<'_, crate::services::terminal::TerminalState>> {
542 let terminal_id = self.terminal_buffers.get(&self.active_buffer())?;
543 let handle = self.terminal_manager.get(*terminal_id)?;
544 handle.state.lock().ok()
545 }
546
547 pub fn send_terminal_input(&mut self, data: &[u8]) {
550 if let Some(&terminal_id) = self.terminal_buffers.get(&self.active_buffer()) {
551 if let Some(handle) = self.terminal_manager.get(terminal_id) {
552 handle.write(data);
553 }
554 }
555 }
556
557 pub fn send_terminal_key(
561 &mut self,
562 code: crossterm::event::KeyCode,
563 modifiers: crossterm::event::KeyModifiers,
564 ) {
565 let app_cursor = self
566 .get_active_terminal_state()
567 .map(|s| s.is_app_cursor())
568 .unwrap_or(false);
569 if let Some(bytes) =
570 crate::services::terminal::pty::key_to_pty_bytes(code, modifiers, app_cursor)
571 {
572 self.send_terminal_input(&bytes);
573 }
574 }
575
576 pub fn send_terminal_mouse(
578 &mut self,
579 col: u16,
580 row: u16,
581 kind: crate::input::handler::TerminalMouseEventKind,
582 modifiers: crossterm::event::KeyModifiers,
583 ) {
584 use crate::input::handler::TerminalMouseEventKind;
585
586 let use_sgr = self
588 .get_active_terminal_state()
589 .map(|s| s.uses_sgr_mouse())
590 .unwrap_or(true);
591
592 let uses_alt_scroll = self
594 .get_active_terminal_state()
595 .map(|s| s.uses_alternate_scroll())
596 .unwrap_or(false);
597
598 if uses_alt_scroll {
599 match kind {
600 TerminalMouseEventKind::ScrollUp => {
601 for _ in 0..3 {
602 self.send_terminal_input(b"\x1b[A");
603 }
604 return;
605 }
606 TerminalMouseEventKind::ScrollDown => {
607 for _ in 0..3 {
608 self.send_terminal_input(b"\x1b[B");
609 }
610 return;
611 }
612 _ => {}
613 }
614 }
615
616 let bytes = if use_sgr {
617 encode_sgr_mouse(col, row, kind, modifiers)
618 } else {
619 encode_x10_mouse(col, row, kind, modifiers)
620 };
621
622 if let Some(bytes) = bytes {
623 self.send_terminal_input(&bytes);
624 }
625 }
626
627 pub fn is_terminal_in_alternate_screen(&self, buffer_id: BufferId) -> bool {
630 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
631 if let Some(handle) = self.terminal_manager.get(terminal_id) {
632 if let Ok(state) = handle.state.lock() {
633 return state.is_alternate_screen();
634 }
635 }
636 }
637 false
638 }
639
640 pub fn resize_terminal(&mut self, buffer_id: BufferId, cols: u16, rows: u16) {
643 if let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) {
644 if let Some(handle) = self.terminal_manager.get_mut(terminal_id) {
645 handle.resize(cols, rows);
646 }
647 }
648 }
649
650 pub fn resize_visible_terminals(&mut self) {
654 let file_explorer_width = if self.file_explorer_visible {
656 self.file_explorer_width.to_cols(self.terminal_width)
657 } else {
658 0
659 };
660 let editor_width = self.terminal_width.saturating_sub(file_explorer_width);
661 let editor_area = ratatui::layout::Rect::new(
662 file_explorer_width,
663 1, editor_width,
665 self.terminal_height.saturating_sub(2), );
667
668 let Some((mgr, _)) = self.buffers.splits() else {
669 return;
670 };
671 let visible_buffers = mgr.get_visible_buffers(editor_area);
672
673 for (_split_id, buffer_id, split_area) in visible_buffers {
674 if self.terminal_buffers.contains_key(&buffer_id) {
675 let content_height = split_area.height.saturating_sub(2);
677 let content_width = split_area.width.saturating_sub(2);
678
679 if content_width > 0 && content_height > 0 {
680 self.resize_terminal(buffer_id, content_width, content_height);
681 }
682 }
683 }
684 }
685
686 pub fn sync_terminal_to_buffer(&mut self, buffer_id: BufferId) {
696 let Some(&terminal_id) = self.terminal_buffers.get(&buffer_id) else {
697 return;
698 };
699 let backing_file = match self.terminal_backing_files.get(&terminal_id) {
701 Some(path) => path.clone(),
702 None => return,
703 };
704
705 if let Some(handle) = self.terminal_manager.get(terminal_id) {
708 if let Ok(mut state) = handle.state.lock() {
709 if let Ok(metadata) = self.resources.authority.filesystem.metadata(&backing_file) {
712 state.set_backing_file_history_end(metadata.size);
713 }
714
715 if let Ok(mut file) = self
717 .resources
718 .authority
719 .filesystem
720 .open_file_for_append(&backing_file)
721 {
722 use std::io::BufWriter;
723 let mut writer = BufWriter::new(&mut *file);
724 if let Err(e) = state.append_visible_screen(&mut writer) {
725 tracing::error!("Failed to append visible screen to backing file: {}", e);
726 }
727 }
728 }
729 }
730
731 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
733 if let Ok(new_state) = EditorState::from_file_with_languages(
734 &backing_file,
735 self.terminal_width,
736 self.terminal_height,
737 large_file_threshold,
738 &self.resources.grammar_registry,
739 &self.resources.config.languages,
740 std::sync::Arc::clone(&self.resources.authority.filesystem),
741 ) {
742 let total_bytes = new_state.buffer.total_bytes();
743 if let Some(state) = self.buffers.get_mut(&buffer_id) {
744 *state = new_state;
745 state.buffer.set_modified(false);
747 }
748 if let Some((mgr, view_states)) = self.buffers.splits_mut() {
750 let active_split = mgr.active_split();
751 if let Some(view_state) = view_states.get_mut(&active_split) {
752 view_state.cursors.primary_mut().position = total_bytes;
753 }
754 }
755 }
756
757 if let Some(state) = self.buffers.get_mut(&buffer_id) {
759 state.editing_disabled = true;
760 state.margins.configure_for_line_numbers(false);
761 }
762
763 let active_split = self
766 .buffers
767 .splits()
768 .expect("active window must have a populated split layout")
769 .0
770 .active_split();
771 self.enter_terminal_scrollback_view(buffer_id, active_split);
772 }
773}
774
775impl Editor {
776 pub fn is_terminal_mode(&self) -> bool {
778 self.active_window().terminal_mode
779 }
780
781 pub fn is_in_terminal_mode_resume(&self, buffer_id: BufferId) -> bool {
783 self.active_window()
784 .terminal_mode_resume
785 .contains(&buffer_id)
786 }
787
788 pub fn is_keyboard_capture(&self) -> bool {
790 self.active_window().keyboard_capture
791 }
792
793 pub fn set_terminal_jump_to_end_on_output(&mut self, value: bool) {
795 self.config_mut().terminal.jump_to_end_on_output = value;
796 }
797
798 pub fn terminal_manager(&self) -> &crate::services::terminal::TerminalManager {
802 &self
803 .windows
804 .get(&self.active_window)
805 .expect("active window must exist")
806 .terminal_manager
807 }
808
809 pub fn terminal_backing_files(
812 &self,
813 ) -> &std::collections::HashMap<crate::services::terminal::TerminalId, std::path::PathBuf> {
814 &self
815 .windows
816 .get(&self.active_window)
817 .expect("active window must exist")
818 .terminal_backing_files
819 }
820
821 pub fn active_buffer_id(&self) -> BufferId {
823 self.active_buffer()
824 }
825
826 pub fn get_buffer_content(&self, buffer_id: BufferId) -> Option<String> {
828 self.windows
829 .get(&self.active_window)
830 .map(|w| &w.buffers)
831 .expect("active window present")
832 .get(&buffer_id)
833 .and_then(|state| state.buffer.to_string())
834 }
835
836 pub fn get_cursor_position(&self, buffer_id: BufferId) -> Option<usize> {
838 self.windows
840 .get(&self.active_window)
841 .and_then(|w| w.buffers.splits())
842 .map(|(_, vs)| vs)
843 .expect("active window must have a populated split layout")
844 .values()
845 .find_map(|vs| {
846 if vs.keyed_states.contains_key(&buffer_id) {
847 Some(vs.keyed_states.get(&buffer_id)?.cursors.primary().position)
848 } else {
849 None
850 }
851 })
852 .or_else(|| {
853 self.windows
855 .get(&self.active_window)
856 .and_then(|w| w.buffers.splits())
857 .map(|(_, vs)| vs)
858 .expect("active window must have a populated split layout")
859 .values()
860 .map(|vs| vs.cursors.primary().position)
861 .next()
862 })
863 }
864
865 pub fn render_terminal_splits(
871 &self,
872 frame: &mut ratatui::Frame,
873 split_areas: &[(
874 crate::model::event::LeafId,
875 BufferId,
876 ratatui::layout::Rect,
877 ratatui::layout::Rect,
878 usize,
879 usize,
880 )],
881 ) {
882 for (_split_id, buffer_id, content_rect, _scrollbar_rect, _thumb_start, _thumb_end) in
883 split_areas
884 {
885 if let Some(&terminal_id) = self.active_window().terminal_buffers.get(buffer_id) {
887 let is_active = *buffer_id == self.active_buffer();
891 if is_active && !self.active_window().terminal_mode {
892 continue;
894 }
895 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
897 if let Ok(state) = handle.state.lock() {
898 let cursor_pos = state.cursor_position();
899 let cursor_visible = state.cursor_visible()
901 && is_active
902 && self.active_window().terminal_mode;
903 let (_, rows) = state.size();
904
905 let mut content = Vec::with_capacity(rows as usize);
907 for row in 0..rows {
908 content.push(state.get_line(row));
909 }
910
911 frame.render_widget(ratatui::widgets::Clear, *content_rect);
913
914 render::render_terminal_content(
916 &content,
917 cursor_pos,
918 cursor_visible,
919 *content_rect,
920 frame.buffer_mut(),
921 self.theme.read().unwrap().terminal_fg,
922 self.theme.read().unwrap().terminal_bg,
923 );
924 }
925 }
926 }
927 }
928 }
929}
930
931pub mod render {
933 use crate::services::terminal::TerminalCell;
934 use ratatui::buffer::Buffer;
935 use ratatui::layout::Rect;
936 use ratatui::style::{Color, Modifier, Style};
937
938 pub fn render_terminal_content(
940 content: &[Vec<TerminalCell>],
941 cursor_pos: (u16, u16),
942 cursor_visible: bool,
943 area: Rect,
944 buf: &mut Buffer,
945 default_fg: Color,
946 default_bg: Color,
947 ) {
948 for (row_idx, row) in content.iter().enumerate() {
949 if row_idx as u16 >= area.height {
950 break;
951 }
952
953 let y = area.y + row_idx as u16;
954
955 for (col_idx, cell) in row.iter().enumerate() {
956 if col_idx as u16 >= area.width {
957 break;
958 }
959
960 let x = area.x + col_idx as u16;
961
962 let mut style = Style::default().fg(default_fg).bg(default_bg);
964
965 if let Some((r, g, b)) = cell.fg {
967 style = style.fg(Color::Rgb(r, g, b));
968 }
969
970 if let Some((r, g, b)) = cell.bg {
971 style = style.bg(Color::Rgb(r, g, b));
972 }
973
974 if cell.bold {
976 style = style.add_modifier(Modifier::BOLD);
977 }
978 if cell.italic {
979 style = style.add_modifier(Modifier::ITALIC);
980 }
981 if cell.underline {
982 style = style.add_modifier(Modifier::UNDERLINED);
983 }
984 if cell.inverse {
985 style = style.add_modifier(Modifier::REVERSED);
986 }
987
988 if cursor_visible
990 && row_idx as u16 == cursor_pos.1
991 && col_idx as u16 == cursor_pos.0
992 {
993 style = style.add_modifier(Modifier::REVERSED);
994 }
995
996 buf.set_string(x, y, cell.c.to_string(), style);
997 }
998 }
999 }
1000}
1001
1002fn encode_sgr_mouse(
1005 col: u16,
1006 row: u16,
1007 kind: crate::input::handler::TerminalMouseEventKind,
1008 modifiers: crossterm::event::KeyModifiers,
1009) -> Option<Vec<u8>> {
1010 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
1011
1012 let cx = col + 1;
1014 let cy = row + 1;
1015
1016 let (button_code, is_release) = match kind {
1018 TerminalMouseEventKind::Down(btn) => {
1019 let code = match btn {
1020 TerminalMouseButton::Left => 0,
1021 TerminalMouseButton::Middle => 1,
1022 TerminalMouseButton::Right => 2,
1023 };
1024 (code, false)
1025 }
1026 TerminalMouseEventKind::Up(btn) => {
1027 let code = match btn {
1028 TerminalMouseButton::Left => 0,
1029 TerminalMouseButton::Middle => 1,
1030 TerminalMouseButton::Right => 2,
1031 };
1032 (code, true)
1033 }
1034 TerminalMouseEventKind::Drag(btn) => {
1035 let code = match btn {
1036 TerminalMouseButton::Left => 32, TerminalMouseButton::Middle => 33, TerminalMouseButton::Right => 34, };
1040 (code, false)
1041 }
1042 TerminalMouseEventKind::Moved => (35, false), TerminalMouseEventKind::ScrollUp => (64, false),
1044 TerminalMouseEventKind::ScrollDown => (65, false),
1045 };
1046
1047 let mut cb = button_code;
1049 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
1050 cb += 4;
1051 }
1052 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
1053 cb += 8;
1054 }
1055 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
1056 cb += 16;
1057 }
1058
1059 let terminator = if is_release { 'm' } else { 'M' };
1061 Some(format!("\x1b[<{};{};{}{}", cb, cx, cy, terminator).into_bytes())
1062}
1063
1064fn encode_x10_mouse(
1067 col: u16,
1068 row: u16,
1069 kind: crate::input::handler::TerminalMouseEventKind,
1070 modifiers: crossterm::event::KeyModifiers,
1071) -> Option<Vec<u8>> {
1072 use crate::input::handler::{TerminalMouseButton, TerminalMouseEventKind};
1073
1074 let cx = (col.min(222) + 1 + 32) as u8;
1077 let cy = (row.min(222) + 1 + 32) as u8;
1078
1079 let button_code: u8 = match kind {
1081 TerminalMouseEventKind::Down(btn) | TerminalMouseEventKind::Drag(btn) => match btn {
1082 TerminalMouseButton::Left => 0,
1083 TerminalMouseButton::Middle => 1,
1084 TerminalMouseButton::Right => 2,
1085 },
1086 TerminalMouseEventKind::Up(_) => 3, TerminalMouseEventKind::Moved => 3 + 32,
1088 TerminalMouseEventKind::ScrollUp => 64,
1089 TerminalMouseEventKind::ScrollDown => 65,
1090 };
1091
1092 let mut cb = button_code;
1094 if matches!(kind, TerminalMouseEventKind::Drag(_)) {
1095 cb += 32; }
1097 if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) {
1098 cb += 4;
1099 }
1100 if modifiers.contains(crossterm::event::KeyModifiers::ALT) {
1101 cb += 8;
1102 }
1103 if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
1104 cb += 16;
1105 }
1106
1107 let cb = cb + 32;
1109
1110 Some(vec![0x1b, b'[', b'M', cb, cx, cy])
1111}