1use crate::cell_renderer::Cell;
2use crate::styled_content::{StyledSegment, extract_styled_segments};
3use crate::themes::Theme;
4use anyhow::Result;
5use par_term_emu_core_rust::pty_session::PtySession;
6use par_term_emu_core_rust::terminal::Terminal;
7use parking_lot::Mutex;
8use std::sync::Arc;
9
10pub use par_term_emu_core_rust::terminal::{ClipboardEntry, ClipboardSlot};
12
13#[allow(dead_code)]
15fn ansi_to_rgb(color_idx: u8) -> [u8; 3] {
16 match color_idx {
17 0 => [0, 0, 0], 1 => [205, 0, 0], 2 => [0, 205, 0], 3 => [205, 205, 0], 4 => [0, 0, 238], 5 => [205, 0, 205], 6 => [0, 205, 205], 7 => [229, 229, 229], 8 => [127, 127, 127], 9 => [255, 0, 0], 10 => [0, 255, 0], 11 => [255, 255, 0], 12 => [92, 92, 255], 13 => [255, 0, 255], 14 => [0, 255, 255], 15 => [255, 255, 255], 16..=231 => {
36 let idx = color_idx - 16;
37 let r = (idx / 36) * 51;
38 let g = ((idx % 36) / 6) * 51;
39 let b = (idx % 6) * 51;
40 [r, g, b]
41 }
42 232..=255 => {
44 let gray = 8 + (color_idx - 232) * 10;
45 [gray, gray, gray]
46 }
47 }
48}
49
50pub struct TerminalManager {
52 pty_session: Arc<Mutex<PtySession>>,
54 dimensions: (usize, usize),
56 theme: Theme,
58}
59
60impl TerminalManager {
61 #[allow(dead_code)]
63 pub fn new(cols: usize, rows: usize) -> Result<Self> {
64 Self::new_with_scrollback(cols, rows, 10000)
65 }
66
67 pub fn new_with_scrollback(cols: usize, rows: usize, scrollback_size: usize) -> Result<Self> {
69 log::info!(
70 "Creating terminal with dimensions: {}x{}, scrollback: {}",
71 cols,
72 rows,
73 scrollback_size
74 );
75
76 let pty_session = PtySession::new(cols, rows, scrollback_size);
77 let pty_session = Arc::new(Mutex::new(pty_session));
78
79 Ok(Self {
80 pty_session,
81 dimensions: (cols, rows),
82 theme: Theme::default(),
83 })
84 }
85
86 pub fn set_theme(&mut self, theme: Theme) {
88 self.theme = theme;
89 }
90
91 pub fn set_cell_dimensions(&self, width: u32, height: u32) {
96 let pty = self.pty_session.lock();
97 let terminal = pty.terminal();
98 let mut term = terminal.lock();
99 term.set_cell_dimensions(width, height);
100 }
101
102 #[allow(dead_code)]
104 pub fn spawn_shell(&mut self) -> Result<()> {
105 log::info!("Spawning shell in PTY");
106 let mut pty = self.pty_session.lock();
107 pty.spawn_shell()
108 .map_err(|e| anyhow::anyhow!("Failed to spawn shell: {}", e))?;
109 Ok(())
110 }
111
112 #[allow(dead_code)]
117 pub fn spawn_custom_shell(&mut self, command: &str) -> Result<()> {
118 log::info!("Spawning custom shell: {}", command);
119 let mut pty = self.pty_session.lock();
120 let args: Vec<&str> = Vec::new();
121 pty.spawn(command, &args)
122 .map_err(|e| anyhow::anyhow!("Failed to spawn custom shell: {}", e))?;
123 Ok(())
124 }
125
126 #[allow(dead_code)]
132 pub fn spawn_custom_shell_with_args(&mut self, command: &str, args: &[String]) -> Result<()> {
133 log::info!("Spawning custom shell: {} with args: {:?}", command, args);
134 let mut pty = self.pty_session.lock();
135 let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
136 pty.spawn(command, &args_refs)
137 .map_err(|e| anyhow::anyhow!("Failed to spawn custom shell: {}", e))?;
138 Ok(())
139 }
140
141 #[allow(dead_code)]
147 pub fn spawn_shell_with_dir(
148 &mut self,
149 working_dir: Option<&str>,
150 env_vars: Option<&std::collections::HashMap<String, String>>,
151 ) -> Result<()> {
152 log::info!(
153 "Spawning shell with dir: {:?}, env: {:?}",
154 working_dir,
155 env_vars
156 );
157 let mut pty = self.pty_session.lock();
158 pty.spawn_shell_with_env(env_vars, working_dir)
159 .map_err(|e| anyhow::anyhow!("Failed to spawn shell with env: {}", e))
160 }
161
162 pub fn spawn_custom_shell_with_dir(
170 &mut self,
171 command: &str,
172 args: Option<&[String]>,
173 working_dir: Option<&str>,
174 env_vars: Option<&std::collections::HashMap<String, String>>,
175 ) -> Result<()> {
176 log::info!(
177 "Spawning custom shell: {} with dir: {:?}, env: {:?}",
178 command,
179 working_dir,
180 env_vars
181 );
182
183 let args_refs: Vec<&str> = args
184 .map(|a| a.iter().map(|s| s.as_str()).collect())
185 .unwrap_or_default();
186
187 let mut pty = self.pty_session.lock();
188 pty.spawn_with_env(command, &args_refs, env_vars, working_dir)
189 .map_err(|e| anyhow::anyhow!("Failed to spawn custom shell with env: {}", e))
190 }
191
192 pub fn write(&self, data: &[u8]) -> Result<()> {
194 if !data.is_empty() {
196 log::debug!(
197 "Writing to PTY: {:?} (bytes: {:?})",
198 String::from_utf8_lossy(data),
199 data
200 );
201 }
202 let mut pty = self.pty_session.lock();
203 pty.write(data)
204 .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
205 Ok(())
206 }
207
208 #[allow(dead_code)]
210 pub fn write_str(&self, data: &str) -> Result<()> {
211 let mut pty = self.pty_session.lock();
212 pty.write_str(data)
213 .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
214 Ok(())
215 }
216
217 #[allow(dead_code)]
219 pub fn content(&self) -> Result<String> {
220 let pty = self.pty_session.lock();
221 Ok(pty.content())
222 }
223
224 #[allow(dead_code)]
226 pub fn resize(&mut self, cols: usize, rows: usize) -> Result<()> {
227 log::info!("Resizing terminal to: {}x{}", cols, rows);
228
229 let mut pty = self.pty_session.lock();
230 pty.resize(cols as u16, rows as u16)
231 .map_err(|e| anyhow::anyhow!("Failed to resize PTY: {}", e))?;
232
233 self.dimensions = (cols, rows);
234 Ok(())
235 }
236
237 pub fn resize_with_pixels(
241 &mut self,
242 cols: usize,
243 rows: usize,
244 width_px: usize,
245 height_px: usize,
246 ) -> Result<()> {
247 log::info!(
248 "Resizing terminal to: {}x{} ({}x{} pixels)",
249 cols,
250 rows,
251 width_px,
252 height_px
253 );
254
255 let mut pty = self.pty_session.lock();
256 pty.resize_with_pixels(cols as u16, rows as u16, width_px as u16, height_px as u16)
257 .map_err(|e| anyhow::anyhow!("Failed to resize PTY with pixels: {}", e))?;
258
259 self.dimensions = (cols, rows);
260 Ok(())
261 }
262
263 #[allow(dead_code)]
265 pub fn set_pixel_size(&mut self, width_px: usize, height_px: usize) -> Result<()> {
266 let pty = self.pty_session.lock();
267 let term_arc = pty.terminal();
268 let mut term = term_arc.lock();
269 term.set_pixel_size(width_px, height_px);
270 Ok(())
271 }
272
273 #[allow(dead_code)]
275 pub fn dimensions(&self) -> (usize, usize) {
276 self.dimensions
277 }
278
279 #[allow(dead_code)]
281 pub fn terminal(&self) -> Arc<Mutex<Terminal>> {
282 let pty = self.pty_session.lock();
283 pty.terminal()
284 }
285
286 #[allow(dead_code)]
288 pub fn has_updates(&self) -> bool {
289 true
292 }
293
294 pub fn is_running(&self) -> bool {
296 let pty = self.pty_session.lock();
297 pty.is_running()
298 }
299
300 pub fn kill(&mut self) -> Result<()> {
302 let mut pty = self.pty_session.lock();
303 pty.kill()
304 .map_err(|e| anyhow::anyhow!("Failed to kill PTY: {:?}", e))
305 }
306
307 pub fn bell_count(&self) -> u64 {
309 let pty = self.pty_session.lock();
310 pty.bell_count()
311 }
312
313 #[allow(dead_code)]
315 pub fn scrollback(&self) -> Vec<String> {
316 let pty = self.pty_session.lock();
317 pty.scrollback()
318 }
319
320 pub fn scrollback_len(&self) -> usize {
322 let pty = self.pty_session.lock();
323 pty.scrollback_len()
324 }
325
326 pub fn clear_scrollback(&self) {
331 let pty = self.pty_session.lock();
332 let terminal = pty.terminal();
333 let mut term = terminal.lock();
334 term.process(b"\x1b[3J");
336 }
337
338 pub fn take_notifications(&self) -> Vec<par_term_emu_core_rust::terminal::Notification> {
340 let pty = self.pty_session.lock();
341 let terminal = pty.terminal();
342 let mut term = terminal.lock();
343 term.take_notifications()
344 }
345
346 pub fn has_notifications(&self) -> bool {
348 let pty = self.pty_session.lock();
349 let terminal = pty.terminal();
350 let term = terminal.lock();
351 term.has_notifications()
352 }
353
354 pub fn screenshot_to_file(
361 &self,
362 path: &std::path::Path,
363 format: &str,
364 scrollback_lines: usize,
365 ) -> Result<()> {
366 use par_term_emu_core_rust::screenshot::{ImageFormat, ScreenshotConfig};
367
368 log::info!(
369 "Taking screenshot to: {} (format: {}, scrollback: {})",
370 path.display(),
371 format,
372 scrollback_lines
373 );
374
375 let pty = self.pty_session.lock();
376 let terminal = pty.terminal();
377 let term = terminal.lock();
378
379 let image_format = match format.to_lowercase().as_str() {
381 "png" => ImageFormat::Png,
382 "jpeg" | "jpg" => ImageFormat::Jpeg,
383 "svg" => ImageFormat::Svg,
384 _ => {
385 log::warn!("Unknown format '{}', defaulting to PNG", format);
386 ImageFormat::Png
387 }
388 };
389
390 let config = ScreenshotConfig {
392 format: image_format,
393 ..Default::default()
394 };
395
396 term.screenshot_to_file(path, config, scrollback_lines)
398 .map_err(|e| anyhow::anyhow!("Failed to save screenshot: {}", e))?;
399
400 log::info!("Screenshot saved successfully");
401 Ok(())
402 }
403
404 pub fn shell_integration_cwd(&self) -> Option<String> {
468 let pty = self.pty_session.lock();
469 let terminal = pty.terminal();
470 let term = terminal.lock();
471 term.shell_integration().cwd().map(String::from)
472 }
473
474 pub fn shell_integration_exit_code(&self) -> Option<i32> {
476 let pty = self.pty_session.lock();
477 let terminal = pty.terminal();
478 let term = terminal.lock();
479 term.shell_integration().exit_code()
480 }
481
482 #[allow(dead_code)]
484 pub fn shell_integration_command(&self) -> Option<String> {
485 let pty = self.pty_session.lock();
486 let terminal = pty.terminal();
487 let term = terminal.lock();
488 term.shell_integration().command().map(String::from)
489 }
490
491 #[allow(dead_code)]
507 pub fn get_graphics(&self) -> Vec<par_term_emu_core_rust::graphics::TerminalGraphic> {
508 let pty = self.pty_session.lock();
509 let terminal = pty.terminal();
510 let term = terminal.lock();
511 let graphics: Vec<_> = term.all_graphics().to_vec();
512 if !graphics.is_empty() {
513 debug_info!(
514 "TERMINAL",
515 "Returning {} graphics from core library",
516 graphics.len()
517 );
518 for (i, g) in graphics.iter().enumerate() {
519 debug_trace!(
520 "TERMINAL",
521 " [{}] protocol={:?}, pos=({},{}), size={}x{}",
522 i,
523 g.protocol,
524 g.position.0,
525 g.position.1,
526 g.width,
527 g.height
528 );
529 }
530 }
531 graphics
532 }
533
534 #[allow(dead_code)]
536 pub fn get_graphics_at_row(
537 &self,
538 row: usize,
539 ) -> Vec<par_term_emu_core_rust::graphics::TerminalGraphic> {
540 let pty = self.pty_session.lock();
541 let terminal = pty.terminal();
542 let term = terminal.lock();
543 term.graphics_at_row(row)
544 .iter()
545 .map(|g| (*g).clone())
546 .collect()
547 }
548
549 #[allow(dead_code)]
551 pub fn graphics_count(&self) -> usize {
552 let pty = self.pty_session.lock();
553 let terminal = pty.terminal();
554 let term = terminal.lock();
555 term.graphics_count()
556 }
557
558 pub fn get_all_hyperlinks(&self) -> Vec<par_term_emu_core_rust::terminal::HyperlinkInfo> {
560 let pty = self.pty_session.lock();
561 let terminal = pty.terminal();
562 let term = terminal.lock();
563 term.get_all_hyperlinks()
564 }
565
566 #[allow(dead_code)]
568 pub fn get_hyperlink_url(&self, hyperlink_id: u32) -> Option<String> {
569 let pty = self.pty_session.lock();
570 let terminal = pty.terminal();
571 let term = terminal.lock();
572 term.get_hyperlink_url(hyperlink_id)
573 }
574
575 pub fn get_scrollback_graphics(
577 &self,
578 ) -> Vec<par_term_emu_core_rust::graphics::TerminalGraphic> {
579 let pty = self.pty_session.lock();
580 let terminal = pty.terminal();
581 let term = terminal.lock();
582 term.all_scrollback_graphics().to_vec()
583 }
584
585 pub fn update_animations(&self) -> bool {
591 let pty = self.pty_session.lock();
592 let terminal = pty.terminal();
593 let mut term = terminal.lock();
594 let changed_images = term.graphics_store_mut().update_animations();
595 !changed_images.is_empty()
596 }
597
598 pub fn get_graphics_with_animations(
603 &self,
604 ) -> Vec<par_term_emu_core_rust::graphics::TerminalGraphic> {
605 let pty = self.pty_session.lock();
606 let terminal = pty.terminal();
607 let term = terminal.lock();
608
609 let mut graphics = Vec::new();
610
611 let base_graphics: Vec<_> = term.all_graphics().to_vec();
613
614 debug_log!(
615 "TERMINAL",
616 "get_graphics_with_animations() - base_graphics count: {}",
617 base_graphics.len()
618 );
619
620 for (idx, graphic) in base_graphics.iter().enumerate() {
622 debug_trace!(
623 "TERMINAL",
624 "Processing graphic {} - pos=({},{}), size={}x{}, kitty_id={:?}",
625 idx,
626 graphic.position.0,
627 graphic.position.1,
628 graphic.width,
629 graphic.height,
630 graphic.kitty_image_id
631 );
632
633 if let Some(image_id) = graphic.kitty_image_id
635 && let Some(anim) = term.graphics_store().get_animation(image_id)
636 && let Some(current_frame) = anim.current_frame()
637 {
638 let mut animated_graphic = graphic.clone();
640 animated_graphic.pixels = current_frame.pixels.clone();
641 animated_graphic.width = current_frame.width;
642 animated_graphic.height = current_frame.height;
643
644 debug_info!(
645 "TERMINAL",
646 "Using animated frame {} for image {}",
647 anim.current_frame,
648 image_id
649 );
650
651 graphics.push(animated_graphic);
652 continue;
653 }
654 debug_trace!("TERMINAL", "Using static graphic {}", idx);
656 graphics.push(graphic.clone());
657 }
658
659 debug_log!("TERMINAL", "Returning {} graphics total", graphics.len());
660 graphics
661 }
662
663 #[allow(dead_code)]
665 pub fn cursor_position(&self) -> (usize, usize) {
666 let pty = self.pty_session.lock();
667 pty.cursor_position()
668 }
669
670 pub fn cursor_style(&self) -> par_term_emu_core_rust::cursor::CursorStyle {
672 let pty = self.pty_session.lock();
673 let terminal = pty.terminal();
674 let term = terminal.lock();
675 term.cursor().style()
676 }
677
678 pub fn is_cursor_visible(&self) -> bool {
683 let pty = self.pty_session.lock();
684 let terminal = pty.terminal();
685 let term = terminal.lock();
686 term.cursor().visible
687 }
688
689 pub fn is_mouse_tracking_enabled(&self) -> bool {
691 let pty = self.pty_session.lock();
692 let terminal = pty.terminal();
693 let term = terminal.lock();
694 !matches!(
695 term.mouse_mode(),
696 par_term_emu_core_rust::mouse::MouseMode::Off
697 )
698 }
699
700 pub fn is_alt_screen_active(&self) -> bool {
706 let pty = self.pty_session.lock();
707 let terminal = pty.terminal();
708 let term = terminal.lock();
709 term.is_alt_screen_active()
710 }
711
712 pub fn get_title(&self) -> String {
717 let pty = self.pty_session.lock();
718 let terminal = pty.terminal();
719 let term = terminal.lock();
720 term.title().to_string()
721 }
722
723 pub fn should_report_mouse_motion(&self, button_pressed: bool) -> bool {
726 let pty = self.pty_session.lock();
727 let terminal = pty.terminal();
728 let term = terminal.lock();
729
730 match term.mouse_mode() {
731 par_term_emu_core_rust::mouse::MouseMode::AnyEvent => true,
732 par_term_emu_core_rust::mouse::MouseMode::ButtonEvent => button_pressed,
733 _ => false,
734 }
735 }
736
737 pub fn encode_mouse_event(
749 &self,
750 button: u8,
751 col: usize,
752 row: usize,
753 pressed: bool,
754 modifiers: u8,
755 ) -> Vec<u8> {
756 let pty = self.pty_session.lock();
757 let terminal = pty.terminal();
758 let mut term = terminal.lock();
759
760 let mouse_event =
761 par_term_emu_core_rust::mouse::MouseEvent::new(button, col, row, pressed, modifiers);
762 term.report_mouse(mouse_event)
763 }
764
765 #[allow(dead_code)]
767 pub fn get_styled_segments(&self) -> Vec<StyledSegment> {
768 let pty = self.pty_session.lock();
769 let terminal = pty.terminal();
770 let term = terminal.lock();
771 let grid = term.active_grid();
772 extract_styled_segments(grid)
773 }
774
775 pub fn update_generation(&self) -> u64 {
780 let pty = self.pty_session.lock();
781 pty.update_generation()
782 }
783
784 pub fn get_cells_with_scrollback(
792 &self,
793 scroll_offset: usize,
794 selection: Option<((usize, usize), (usize, usize))>,
795 rectangular: bool,
796 _cursor: Option<((usize, usize), f32)>,
797 ) -> Vec<Cell> {
798 let pty = self.pty_session.lock();
799 let terminal = pty.terminal();
800 let term = terminal.lock();
801 let grid = term.active_grid();
802
803 let cursor_with_style = None;
805
806 let rows = grid.rows();
807 let cols = grid.cols();
808 let scrollback_len = grid.scrollback_len();
809 let clamped_offset = scroll_offset.min(scrollback_len);
810 let total_lines = scrollback_len + rows;
811 let end_line = total_lines.saturating_sub(clamped_offset);
812 let start_line = end_line.saturating_sub(rows);
813
814 let mut cells = Vec::with_capacity(rows * cols);
815
816 for line_idx in start_line..end_line {
817 let screen_row = line_idx - start_line;
818
819 if line_idx < scrollback_len {
820 if let Some(line) = grid.scrollback_line(line_idx) {
821 Self::push_line_from_slice(
822 line,
823 cols,
824 &mut cells,
825 screen_row,
826 selection,
827 rectangular,
828 cursor_with_style,
829 &self.theme,
830 );
831 } else {
832 Self::push_empty_cells(cols, &mut cells);
833 }
834 } else {
835 let grid_row = line_idx - scrollback_len;
836 Self::push_grid_row(
837 grid,
838 grid_row,
839 cols,
840 &mut cells,
841 screen_row,
842 selection,
843 rectangular,
844 cursor_with_style,
845 &self.theme,
846 );
847 }
848 }
849
850 cells
851 }
852}
853
854impl TerminalManager {
855 #[allow(clippy::too_many_arguments)]
856 fn push_line_from_slice(
857 line: &[par_term_emu_core_rust::cell::Cell],
858 cols: usize,
859 dest: &mut Vec<Cell>,
860 screen_row: usize,
861 selection: Option<((usize, usize), (usize, usize))>,
862 rectangular: bool,
863 cursor: Option<(
864 (usize, usize),
865 f32,
866 par_term_emu_core_rust::cursor::CursorStyle,
867 )>,
868 theme: &Theme,
869 ) {
870 let copy_len = cols.min(line.len());
871 for (col, cell) in line[..copy_len].iter().enumerate() {
872 let is_selected = Self::is_cell_selected(col, screen_row, selection, rectangular);
873 let cursor_info = cursor.and_then(|((cx, cy), opacity, style)| {
874 if cx == col && cy == screen_row {
875 Some((opacity, style))
876 } else {
877 None
878 }
879 });
880 dest.push(Self::convert_term_cell_with_theme(
881 cell,
882 is_selected,
883 cursor_info,
884 theme,
885 ));
886 }
887
888 if copy_len < cols {
889 Self::push_empty_cells(cols - copy_len, dest);
890 }
891 }
892
893 #[allow(clippy::too_many_arguments)]
894 fn push_grid_row(
895 grid: &par_term_emu_core_rust::grid::Grid,
896 row: usize,
897 cols: usize,
898 dest: &mut Vec<Cell>,
899 screen_row: usize,
900 selection: Option<((usize, usize), (usize, usize))>,
901 rectangular: bool,
902 cursor: Option<(
903 (usize, usize),
904 f32,
905 par_term_emu_core_rust::cursor::CursorStyle,
906 )>,
907 theme: &Theme,
908 ) {
909 for col in 0..cols {
910 let is_selected = Self::is_cell_selected(col, screen_row, selection, rectangular);
911 let cursor_info = cursor.and_then(|((cx, cy), opacity, style)| {
912 if cx == col && cy == screen_row {
913 Some((opacity, style))
914 } else {
915 None
916 }
917 });
918 if let Some(cell) = grid.get(col, row) {
919 dest.push(Self::convert_term_cell_with_theme(
920 cell,
921 is_selected,
922 cursor_info,
923 theme,
924 ));
925 } else {
926 dest.push(Cell::default());
927 }
928 }
929 }
930
931 fn push_empty_cells(count: usize, dest: &mut Vec<Cell>) {
932 for _ in 0..count {
933 dest.push(Cell::default());
934 }
935 }
936
937 fn is_cell_selected(
939 col: usize,
940 row: usize,
941 selection: Option<((usize, usize), (usize, usize))>,
942 rectangular: bool,
943 ) -> bool {
944 if let Some(((start_col, start_row), (end_col, end_row))) = selection {
945 if rectangular {
946 let min_col = start_col.min(end_col);
948 let max_col = start_col.max(end_col);
949 let min_row = start_row.min(end_row);
950 let max_row = start_row.max(end_row);
951
952 return col >= min_col && col <= max_col && row >= min_row && row <= max_row;
953 }
954
955 if start_row == end_row {
958 return row == start_row && col >= start_col && col <= end_col;
959 }
960
961 if row == start_row {
963 return col >= start_col;
965 } else if row == end_row {
966 return col <= end_col;
968 } else if row > start_row && row < end_row {
969 return true;
971 }
972 }
973 false
974 }
975
976 fn convert_term_cell_with_theme(
977 term_cell: &par_term_emu_core_rust::cell::Cell,
978 is_selected: bool,
979 cursor_info: Option<(f32, par_term_emu_core_rust::cursor::CursorStyle)>,
980 theme: &Theme,
981 ) -> Cell {
982 use par_term_emu_core_rust::color::{Color as TermColor, NamedColor};
983 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
984
985 let bg_rgb = term_cell.bg.to_rgb();
988 let fg_rgb = term_cell.fg.to_rgb();
989 let has_colored_bg = bg_rgb != (0, 0, 0); let has_reverse = term_cell.flags.reverse();
991
992 if has_colored_bg || has_reverse {
993 debug_info!(
994 "TERMINAL",
995 "Cell with colored BG or REVERSE: '{}' (U+{:04X}): fg={:?} (RGB:{},{},{}), bg={:?} (RGB:{},{},{}), reverse={}, flags={:?}",
996 if term_cell.c.is_control() {
997 '?'
998 } else {
999 term_cell.c
1000 },
1001 term_cell.c as u32,
1002 term_cell.fg,
1003 fg_rgb.0,
1004 fg_rgb.1,
1005 fg_rgb.2,
1006 term_cell.bg,
1007 bg_rgb.0,
1008 bg_rgb.1,
1009 bg_rgb.2,
1010 has_reverse,
1011 term_cell.flags
1012 );
1013 }
1014
1015 let fg = match &term_cell.fg {
1017 TermColor::Named(named) => {
1018 #[allow(unreachable_patterns)]
1019 let theme_color = match named {
1020 NamedColor::Black => theme.black,
1021 NamedColor::Red => theme.red,
1022 NamedColor::Green => theme.green,
1023 NamedColor::Yellow => theme.yellow,
1024 NamedColor::Blue => theme.blue,
1025 NamedColor::Magenta => theme.magenta,
1026 NamedColor::Cyan => theme.cyan,
1027 NamedColor::White => theme.white,
1028 NamedColor::BrightBlack => theme.bright_black,
1029 NamedColor::BrightRed => theme.bright_red,
1030 NamedColor::BrightGreen => theme.bright_green,
1031 NamedColor::BrightYellow => theme.bright_yellow,
1032 NamedColor::BrightBlue => theme.bright_blue,
1033 NamedColor::BrightMagenta => theme.bright_magenta,
1034 NamedColor::BrightCyan => theme.bright_cyan,
1035 NamedColor::BrightWhite => theme.bright_white,
1036 _ => theme.foreground, };
1038 (theme_color.r, theme_color.g, theme_color.b)
1039 }
1040 _ => term_cell.fg.to_rgb(), };
1042
1043 let bg = match &term_cell.bg {
1044 TermColor::Named(named) => {
1045 #[allow(unreachable_patterns)]
1046 let theme_color = match named {
1047 NamedColor::Black => theme.black,
1048 NamedColor::Red => theme.red,
1049 NamedColor::Green => theme.green,
1050 NamedColor::Yellow => theme.yellow,
1051 NamedColor::Blue => theme.blue,
1052 NamedColor::Magenta => theme.magenta,
1053 NamedColor::Cyan => theme.cyan,
1054 NamedColor::White => theme.white,
1055 NamedColor::BrightBlack => theme.bright_black,
1056 NamedColor::BrightRed => theme.bright_red,
1057 NamedColor::BrightGreen => theme.bright_green,
1058 NamedColor::BrightYellow => theme.bright_yellow,
1059 NamedColor::BrightBlue => theme.bright_blue,
1060 NamedColor::BrightMagenta => theme.bright_magenta,
1061 NamedColor::BrightCyan => theme.bright_cyan,
1062 NamedColor::BrightWhite => theme.bright_white,
1063 _ => theme.background, };
1065 (theme_color.r, theme_color.g, theme_color.b)
1066 }
1067 _ => term_cell.bg.to_rgb(), };
1069
1070 let is_reverse = term_cell.flags.reverse();
1072
1073 let (fg_color, bg_color) = if let Some((opacity, style)) = cursor_info {
1075 let blend = |normal: u8, inverted: u8, opacity: f32| -> u8 {
1077 (normal as f32 * (1.0 - opacity) + inverted as f32 * opacity) as u8
1078 };
1079
1080 match style {
1084 TermCursorStyle::SteadyBlock | TermCursorStyle::BlinkingBlock => (
1086 [
1087 blend(fg.0, bg.0, opacity),
1088 blend(fg.1, bg.1, opacity),
1089 blend(fg.2, bg.2, opacity),
1090 255,
1091 ],
1092 [
1093 blend(bg.0, fg.0, opacity),
1094 blend(bg.1, fg.1, opacity),
1095 blend(bg.2, fg.2, opacity),
1096 255,
1097 ],
1098 ),
1099 TermCursorStyle::SteadyBar
1102 | TermCursorStyle::BlinkingBar
1103 | TermCursorStyle::SteadyUnderline
1104 | TermCursorStyle::BlinkingUnderline => (
1105 [
1106 blend(fg.0, bg.0, opacity),
1107 blend(fg.1, bg.1, opacity),
1108 blend(fg.2, bg.2, opacity),
1109 255,
1110 ],
1111 [
1112 blend(bg.0, fg.0, opacity),
1113 blend(bg.1, fg.1, opacity),
1114 blend(bg.2, fg.2, opacity),
1115 255,
1116 ],
1117 ),
1118 }
1119 } else if is_selected || is_reverse {
1120 (
1122 [bg.0, bg.1, bg.2, 255], [fg.0, fg.1, fg.2, 255], )
1125 } else {
1126 ([fg.0, fg.1, fg.2, 255], [bg.0, bg.1, bg.2, 255])
1128 };
1129
1130 let grapheme = if term_cell.has_combining_chars() {
1132 term_cell.get_grapheme()
1133 } else {
1134 term_cell.base_char().to_string()
1135 };
1136
1137 Cell {
1138 grapheme,
1139 fg_color,
1140 bg_color,
1141 bold: term_cell.flags.bold(),
1142 italic: term_cell.flags.italic(),
1143 underline: term_cell.flags.underline(),
1144 strikethrough: term_cell.flags.strikethrough(),
1145 hyperlink_id: term_cell.flags.hyperlink_id,
1146 wide_char: term_cell.flags.wide_char(),
1147 wide_char_spacer: term_cell.flags.wide_char_spacer(),
1148 }
1149 }
1150}
1151
1152impl TerminalManager {
1157 pub fn get_clipboard_history(&self, slot: ClipboardSlot) -> Vec<ClipboardEntry> {
1159 let pty = self.pty_session.lock();
1160 let terminal = pty.terminal();
1161 let term = terminal.lock();
1162 term.get_clipboard_history(slot)
1163 }
1164
1165 #[allow(dead_code)]
1167 #[allow(dead_code)]
1168 pub fn get_latest_clipboard(&self, slot: ClipboardSlot) -> Option<ClipboardEntry> {
1169 let pty = self.pty_session.lock();
1170 let terminal = pty.terminal();
1171 let term = terminal.lock();
1172 term.get_latest_clipboard(slot)
1173 }
1174
1175 #[allow(dead_code)]
1177 #[allow(dead_code)]
1178 pub fn search_clipboard_history(
1179 &self,
1180 query: &str,
1181 slot: Option<ClipboardSlot>,
1182 ) -> Vec<ClipboardEntry> {
1183 let pty = self.pty_session.lock();
1184 let terminal = pty.terminal();
1185 let term = terminal.lock();
1186 term.search_clipboard_history(query, slot)
1187 }
1188
1189 pub fn add_to_clipboard_history(
1191 &self,
1192 slot: ClipboardSlot,
1193 content: String,
1194 label: Option<String>,
1195 ) {
1196 let pty = self.pty_session.lock();
1197 let terminal = pty.terminal();
1198 let mut term = terminal.lock();
1199 term.add_to_clipboard_history(slot, content, label);
1200 }
1201
1202 pub fn clear_clipboard_history(&self, slot: ClipboardSlot) {
1204 let pty = self.pty_session.lock();
1205 let terminal = pty.terminal();
1206 let mut term = terminal.lock();
1207 term.clear_clipboard_history(slot);
1208 }
1209
1210 pub fn clear_all_clipboard_history(&self) {
1212 let pty = self.pty_session.lock();
1213 let terminal = pty.terminal();
1214 let mut term = terminal.lock();
1215 term.clear_all_clipboard_history();
1216 }
1217
1218 pub fn set_max_clipboard_sync_events(&self, max: usize) {
1220 let pty = self.pty_session.lock();
1221 let terminal = pty.terminal();
1222 let mut term = terminal.lock();
1223 term.set_max_clipboard_sync_events(max);
1224 }
1225
1226 pub fn set_max_clipboard_event_bytes(&self, max_bytes: usize) {
1228 let pty = self.pty_session.lock();
1229 let terminal = pty.terminal();
1230 let mut term = terminal.lock();
1231 term.set_max_clipboard_event_bytes(max_bytes);
1232 }
1233
1234 #[allow(dead_code)]
1236 #[allow(dead_code)]
1237 pub fn set_max_clipboard_sync_history(&self, max: usize) {
1238 let pty = self.pty_session.lock();
1239 let terminal = pty.terminal();
1240 let mut term = terminal.lock();
1241 term.set_max_clipboard_sync_history(max);
1242 }
1243}
1244
1245impl Drop for TerminalManager {
1246 fn drop(&mut self) {
1247 log::info!("Shutting down terminal manager");
1248
1249 if let Some(mut pty) = self.pty_session.try_lock() {
1251 if pty.is_running() {
1253 log::info!("Killing PTY process during shutdown");
1254 if let Err(e) = pty.kill() {
1255 log::warn!("Failed to kill PTY process: {:?}", e);
1256 }
1257 }
1258 } else {
1259 log::warn!("Could not acquire PTY lock during terminal manager shutdown");
1260 }
1261
1262 log::info!("Terminal manager shutdown complete");
1263 }
1264}