1use crate::scrollback_metadata::{CommandSnapshot, ScrollbackMark, ScrollbackMetadata};
2use crate::styled_content::{StyledSegment, extract_styled_segments};
3use anyhow::Result;
4use par_term_config::Theme;
5use par_term_emu_core_rust::pty_session::PtySession;
6use par_term_emu_core_rust::shell_integration::ShellIntegrationMarker;
7use par_term_emu_core_rust::terminal::Terminal;
8use parking_lot::Mutex;
9use std::sync::Arc;
10
11pub use par_term_emu_core_rust::terminal::{ClipboardEntry, ClipboardSlot};
13
14pub mod clipboard;
15pub mod graphics;
16pub mod hyperlinks;
17pub mod rendering;
18pub mod spawn;
19
20pub fn coprocess_env() -> std::collections::HashMap<String, String> {
27 use std::sync::OnceLock;
28 static CACHED_PATH: OnceLock<Option<String>> = OnceLock::new();
29
30 let resolved_path = CACHED_PATH.get_or_init(|| {
31 #[cfg(unix)]
32 {
33 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
34 match std::process::Command::new(&shell)
35 .args(["-lc", "printf '%s' \"$PATH\""])
36 .output()
37 {
38 Ok(output) if output.status.success() => {
39 let path = String::from_utf8_lossy(&output.stdout).to_string();
40 if !path.is_empty() {
41 log::debug!("Resolved login shell PATH: {}", path);
42 Some(path)
43 } else {
44 log::warn!("Login shell returned empty PATH");
45 None
46 }
47 }
48 Ok(output) => {
49 log::warn!(
50 "Login shell PATH resolution failed (exit={})",
51 output.status
52 );
53 None
54 }
55 Err(e) => {
56 log::warn!("Failed to run login shell for PATH resolution: {}", e);
57 None
58 }
59 }
60 }
61 #[cfg(not(unix))]
62 {
63 None
64 }
65 });
66
67 let mut env = std::collections::HashMap::new();
68 if let Some(path) = resolved_path {
69 env.insert("PATH".to_string(), path.clone());
70 }
71 env
72}
73
74pub struct TerminalManager {
76 pub(crate) pty_session: Arc<Mutex<PtySession>>,
78 pub(crate) dimensions: (usize, usize),
80 pub(crate) theme: Theme,
82 pub(crate) scrollback_metadata: ScrollbackMetadata,
84 last_shell_marker: Option<ShellIntegrationMarker>,
86 command_start_pos: Option<(usize, usize)>,
91 captured_command_text: Option<(usize, String)>,
94}
95
96impl TerminalManager {
97 #[allow(dead_code)]
99 pub fn new(cols: usize, rows: usize) -> Result<Self> {
100 Self::new_with_scrollback(cols, rows, 10000)
101 }
102
103 pub fn new_with_scrollback(cols: usize, rows: usize, scrollback_size: usize) -> Result<Self> {
105 log::info!(
106 "Creating terminal with dimensions: {}x{}, scrollback: {}",
107 cols,
108 rows,
109 scrollback_size
110 );
111
112 let pty_session = PtySession::new(cols, rows, scrollback_size);
113 let pty_session = Arc::new(Mutex::new(pty_session));
114
115 Ok(Self {
116 pty_session,
117 dimensions: (cols, rows),
118 theme: Theme::default(),
119 scrollback_metadata: ScrollbackMetadata::new(),
120 last_shell_marker: None,
121 command_start_pos: None,
122 captured_command_text: None,
123 })
124 }
125
126 pub fn set_theme(&mut self, theme: Theme) {
128 self.theme = theme;
129 }
130
131 pub fn update_scrollback_metadata(&mut self, scrollback_len: usize, cursor_row: usize) {
142 let pty = self.pty_session.lock();
143 let terminal = pty.terminal();
144 let mut term = terminal.lock();
145
146 let shell_events = term.poll_shell_integration_events();
148
149 let history = term.get_command_history();
151 let history_len = history.len();
152 let last_command = history
153 .last()
154 .map(|c| CommandSnapshot::from_core(c, history_len.saturating_sub(1)));
155
156 if !shell_events.is_empty() {
158 for (event_type, event_command, exit_code, _timestamp, cursor_line) in &shell_events {
159 let marker = match event_type.as_str() {
160 "prompt_start" => Some(ShellIntegrationMarker::PromptStart),
161 "command_start" => Some(ShellIntegrationMarker::CommandStart),
162 "command_executed" => Some(ShellIntegrationMarker::CommandExecuted),
163 "command_finished" => Some(ShellIntegrationMarker::CommandFinished),
164 _ => None,
165 };
166
167 let abs_line = cursor_line.unwrap_or(scrollback_len + cursor_row);
168
169 let prev_marker = self.last_shell_marker;
171 if marker != prev_marker {
172 let cursor_col = term.cursor().col;
173 match marker {
174 Some(ShellIntegrationMarker::CommandStart) => {
175 self.command_start_pos = Some((abs_line, cursor_col));
176 }
177 _ => {
178 if let Some((start_abs_line, start_col)) = self.command_start_pos.take()
179 {
180 let text = Self::extract_command_text(
181 &term,
182 start_abs_line,
183 start_col,
184 scrollback_len,
185 );
186 if !text.is_empty() {
187 self.captured_command_text = Some((start_abs_line, text));
188 }
189 }
190 }
191 }
192 self.last_shell_marker = marker;
193 }
194
195 let is_finished = matches!(marker, Some(ShellIntegrationMarker::CommandFinished));
197
198 self.scrollback_metadata.apply_event(
199 marker,
200 abs_line,
201 if is_finished { history_len } else { 0 },
202 if is_finished {
203 last_command.clone()
204 } else {
205 None
206 },
207 if is_finished { *exit_code } else { None },
208 );
209
210 match event_type.as_str() {
212 "command_executed" => {
213 let cmd_text = event_command
214 .clone()
215 .or_else(|| self.captured_command_text.as_ref().map(|(_, t)| t.clone()))
216 .unwrap_or_default();
217 if !cmd_text.is_empty() {
218 term.start_command_execution(cmd_text);
219 }
220 }
221 "command_finished" => {
222 term.end_command_execution(*exit_code);
223 }
224 _ => {}
225 }
226 }
227 }
228
229 drop(term);
230 drop(terminal);
231 drop(pty);
232
233 if let Some((abs_line, cmd)) = self.captured_command_text.take() {
236 self.scrollback_metadata.set_mark_command_at(abs_line, cmd);
237 }
238 }
239
240 fn extract_command_text(
242 term: &Terminal,
243 start_abs_line: usize,
244 start_col: usize,
245 current_scrollback_len: usize,
246 ) -> String {
247 let grid = term.active_grid();
248 let mut parts = Vec::new();
249 for offset in 0..5 {
250 let abs_line = start_abs_line + offset;
251 let (text, is_wrapped) = if abs_line < current_scrollback_len {
252 let t = Self::scrollback_line_text(grid, abs_line);
253 let w = grid.is_scrollback_wrapped(abs_line);
254 (t, w)
255 } else {
256 let grid_row = abs_line - current_scrollback_len;
257 let t = grid.row_text(grid_row);
258 let w = grid.is_line_wrapped(grid_row);
259 (t, w)
260 };
261 let trimmed = if offset == 0 {
262 text.chars()
263 .skip(start_col)
264 .collect::<String>()
265 .trim_end()
266 .to_string()
267 } else {
268 text.trim_end().to_string()
269 };
270 if !trimmed.is_empty() {
271 parts.push(trimmed);
272 }
273 if !is_wrapped {
274 break;
275 }
276 }
277 parts.join("").trim().to_string()
278 }
279
280 fn scrollback_line_text(
282 grid: &par_term_emu_core_rust::grid::Grid,
283 scrollback_index: usize,
284 ) -> String {
285 if let Some(cells) = grid.scrollback_line(scrollback_index) {
286 cells
287 .iter()
288 .filter(|cell| !cell.flags.wide_char_spacer())
289 .map(|cell| cell.get_grapheme())
290 .collect::<Vec<String>>()
291 .join("")
292 } else {
293 String::new()
294 }
295 }
296
297 pub fn scrollback_marks(&self) -> Vec<ScrollbackMark> {
299 self.scrollback_metadata.marks()
300 }
301
302 pub fn scrollback_previous_mark(&self, line: usize) -> Option<usize> {
304 self.scrollback_metadata.previous_mark(line)
305 }
306
307 pub fn scrollback_next_mark(&self, line: usize) -> Option<usize> {
309 self.scrollback_metadata.next_mark(line)
310 }
311
312 pub fn core_command_history(&self) -> Vec<(String, Option<i32>, Option<u64>)> {
316 let pty = self.pty_session.lock();
317 let terminal = pty.terminal();
318 let term = terminal.lock();
319 term.get_command_history()
320 .iter()
321 .map(|cmd| (cmd.command.clone(), cmd.exit_code, cmd.duration_ms))
322 .collect()
323 }
324
325 pub fn set_cell_dimensions(&self, width: u32, height: u32) {
327 let pty = self.pty_session.lock();
328 let terminal = pty.terminal();
329 let mut term = terminal.lock();
330 term.set_cell_dimensions(width, height);
331 }
332
333 pub fn write(&self, data: &[u8]) -> Result<()> {
335 if !data.is_empty() {
336 log::debug!(
337 "Writing to PTY: {:?} (bytes: {:?})",
338 String::from_utf8_lossy(data),
339 data
340 );
341 }
342 let mut pty = self.pty_session.lock();
343 pty.write(data)
344 .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
345 Ok(())
346 }
347
348 #[allow(dead_code)]
350 pub fn write_str(&self, data: &str) -> Result<()> {
351 let mut pty = self.pty_session.lock();
352 pty.write_str(data)
353 .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
354 Ok(())
355 }
356
357 pub fn process_data(&self, data: &[u8]) {
359 let pty = self.pty_session.lock();
360 let terminal = pty.terminal();
361 let mut term = terminal.lock();
362 term.process(data);
363 }
364
365 pub fn paste(&self, content: &str) -> Result<()> {
367 if content.is_empty() {
368 return Ok(());
369 }
370
371 let content = content.replace('\n', "\r");
372
373 log::debug!("Pasting {} chars (bracketed paste check)", content.len());
374
375 let (start, end) = {
376 let pty = self.pty_session.lock();
377 let terminal = pty.terminal();
378 let term = terminal.lock();
379 (
380 term.bracketed_paste_start().to_vec(),
381 term.bracketed_paste_end().to_vec(),
382 )
383 };
384
385 let mut pty = self.pty_session.lock();
386 if !start.is_empty() {
387 log::debug!("Sending bracketed paste start sequence");
388 pty.write(&start)
389 .map_err(|e| anyhow::anyhow!("Failed to write bracketed paste start: {}", e))?;
390 }
391 pty.write(content.as_bytes())
392 .map_err(|e| anyhow::anyhow!("Failed to write paste content: {}", e))?;
393 if !end.is_empty() {
394 log::debug!("Sending bracketed paste end sequence");
395 pty.write(&end)
396 .map_err(|e| anyhow::anyhow!("Failed to write bracketed paste end: {}", e))?;
397 }
398
399 Ok(())
400 }
401
402 pub async fn paste_with_delay(&self, content: &str, delay_ms: u64) -> Result<()> {
404 if content.is_empty() {
405 return Ok(());
406 }
407
408 let (start, end) = {
409 let pty = self.pty_session.lock();
410 let terminal = pty.terminal();
411 let term = terminal.lock();
412 (
413 term.bracketed_paste_start().to_vec(),
414 term.bracketed_paste_end().to_vec(),
415 )
416 };
417
418 if !start.is_empty() {
419 let mut pty = self.pty_session.lock();
420 pty.write(&start)
421 .map_err(|e| anyhow::anyhow!("Failed to write bracketed paste start: {}", e))?;
422 }
423
424 let lines: Vec<&str> = content.split('\n').collect();
425 let delay = tokio::time::Duration::from_millis(delay_ms);
426
427 for (i, line) in lines.iter().enumerate() {
428 let mut line_data = line.replace('\n', "\r");
429 if i < lines.len() - 1 {
430 line_data.push('\r');
431 }
432
433 {
434 let mut pty = self.pty_session.lock();
435 pty.write(line_data.as_bytes())
436 .map_err(|e| anyhow::anyhow!("Failed to write paste line: {}", e))?;
437 }
438
439 if i < lines.len() - 1 {
440 tokio::time::sleep(delay).await;
441 }
442 }
443
444 if !end.is_empty() {
445 let mut pty = self.pty_session.lock();
446 pty.write(&end)
447 .map_err(|e| anyhow::anyhow!("Failed to write bracketed paste end: {}", e))?;
448 }
449
450 log::debug!(
451 "Pasted {} lines with {}ms delay ({} chars total)",
452 lines.len(),
453 delay_ms,
454 content.len()
455 );
456
457 Ok(())
458 }
459
460 #[allow(dead_code)]
462 pub fn content(&self) -> Result<String> {
463 let pty = self.pty_session.lock();
464 Ok(pty.content())
465 }
466
467 #[allow(dead_code)]
469 pub fn resize(&mut self, cols: usize, rows: usize) -> Result<()> {
470 log::info!("Resizing terminal to: {}x{}", cols, rows);
471
472 let mut pty = self.pty_session.lock();
473 pty.resize(cols as u16, rows as u16)
474 .map_err(|e| anyhow::anyhow!("Failed to resize PTY: {}", e))?;
475
476 self.dimensions = (cols, rows);
477 Ok(())
478 }
479
480 pub fn resize_with_pixels(
482 &mut self,
483 cols: usize,
484 rows: usize,
485 width_px: usize,
486 height_px: usize,
487 ) -> Result<()> {
488 log::info!(
489 "Resizing terminal to: {}x{} ({}x{} pixels)",
490 cols,
491 rows,
492 width_px,
493 height_px
494 );
495
496 let mut pty = self.pty_session.lock();
497 pty.resize_with_pixels(cols as u16, rows as u16, width_px as u16, height_px as u16)
498 .map_err(|e| anyhow::anyhow!("Failed to resize PTY with pixels: {}", e))?;
499
500 self.dimensions = (cols, rows);
501 Ok(())
502 }
503
504 #[allow(dead_code)]
506 pub fn set_pixel_size(&mut self, width_px: usize, height_px: usize) -> Result<()> {
507 let pty = self.pty_session.lock();
508 let term_arc = pty.terminal();
509 let mut term = term_arc.lock();
510 term.set_pixel_size(width_px, height_px);
511 Ok(())
512 }
513
514 #[allow(dead_code)]
516 pub fn dimensions(&self) -> (usize, usize) {
517 self.dimensions
518 }
519
520 #[allow(dead_code)]
522 pub fn terminal(&self) -> Arc<Mutex<Terminal>> {
523 let pty = self.pty_session.lock();
524 pty.terminal()
525 }
526
527 #[allow(dead_code)]
529 pub fn has_updates(&self) -> bool {
530 false
531 }
532
533 pub fn is_running(&self) -> bool {
535 let pty = self.pty_session.lock();
536 pty.is_running()
537 }
538
539 pub fn kill(&mut self) -> Result<()> {
541 let mut pty = self.pty_session.lock();
542 pty.kill()
543 .map_err(|e| anyhow::anyhow!("Failed to kill PTY: {:?}", e))
544 }
545
546 pub fn bell_count(&self) -> u64 {
548 let pty = self.pty_session.lock();
549 pty.bell_count()
550 }
551
552 #[allow(dead_code)]
554 pub fn scrollback(&self) -> Vec<String> {
555 let pty = self.pty_session.lock();
556 pty.scrollback()
557 }
558
559 pub fn scrollback_len(&self) -> usize {
561 let pty = self.pty_session.lock();
562 pty.scrollback_len()
563 }
564
565 pub fn line_text_at_absolute(&self, absolute_line: usize) -> Option<String> {
567 let pty = self.pty_session.lock();
568 let terminal = pty.terminal();
569 let term = terminal.lock();
570 let grid = term.active_grid();
571 let scrollback_len = grid.scrollback_len();
572
573 if absolute_line < scrollback_len {
574 Some(Self::scrollback_line_text(grid, absolute_line))
575 } else {
576 let screen_row = absolute_line - scrollback_len;
577 if screen_row < grid.rows() {
578 Some(grid.row_text(screen_row))
579 } else {
580 None
581 }
582 }
583 }
584
585 pub fn lines_text_range(&self, start: usize, end: usize) -> Vec<(String, usize)> {
587 let pty = self.pty_session.lock();
588 let terminal = pty.terminal();
589 let term = terminal.lock();
590 let grid = term.active_grid();
591 let scrollback_len = grid.scrollback_len();
592 let max_line = scrollback_len + grid.rows();
593
594 let start = start.min(max_line);
595 let end = end.min(max_line);
596
597 let mut result = Vec::with_capacity(end.saturating_sub(start));
598 for abs_line in start..end {
599 let text = if abs_line < scrollback_len {
600 Self::scrollback_line_text(grid, abs_line)
601 } else {
602 let screen_row = abs_line - scrollback_len;
603 if screen_row < grid.rows() {
604 grid.row_text(screen_row)
605 } else {
606 break;
607 }
608 };
609 result.push((text, abs_line));
610 }
611 result
612 }
613
614 pub fn scrollback_as_cells(&self) -> Vec<Vec<par_term_config::Cell>> {
616 let pty = self.pty_session.lock();
617 let terminal = pty.terminal();
618 let term = terminal.lock();
619 let grid = term.active_grid();
620
621 let scrollback_len = grid.scrollback_len();
622 let cols = grid.cols();
623 let mut result = Vec::with_capacity(scrollback_len);
624
625 for line_idx in 0..scrollback_len {
626 let mut row_cells = Vec::with_capacity(cols);
627 if let Some(line) = grid.scrollback_line(line_idx) {
628 Self::push_line_from_slice(
629 line,
630 cols,
631 &mut row_cells,
632 0, None, false, None, &self.theme,
637 );
638 } else {
639 Self::push_empty_cells(cols, &mut row_cells);
640 }
641 result.push(row_cells);
642 }
643
644 result
645 }
646
647 pub fn clear_scrollback(&self) {
649 let pty = self.pty_session.lock();
650 let terminal = pty.terminal();
651 let mut term = terminal.lock();
652 term.process(b"\x1b[3J");
653 }
654
655 pub fn clear_scrollback_metadata(&mut self) {
657 self.scrollback_metadata.clear();
658 self.last_shell_marker = None;
659 self.command_start_pos = None;
660 self.captured_command_text = None;
661 }
662
663 pub fn search(
665 &self,
666 query: &str,
667 case_sensitive: bool,
668 ) -> Vec<par_term_emu_core_rust::terminal::SearchMatch> {
669 let pty = self.pty_session.lock();
670 let terminal = pty.terminal();
671 let term = terminal.lock();
672 term.search_text(query, case_sensitive)
673 }
674
675 pub fn search_scrollback(
677 &self,
678 query: &str,
679 case_sensitive: bool,
680 max_lines: Option<usize>,
681 ) -> Vec<par_term_emu_core_rust::terminal::SearchMatch> {
682 let pty = self.pty_session.lock();
683 let terminal = pty.terminal();
684 let term = terminal.lock();
685 term.search_scrollback(query, case_sensitive, max_lines)
686 }
687
688 pub fn search_all(&self, query: &str, case_sensitive: bool) -> Vec<crate::SearchMatch> {
690 let pty = self.pty_session.lock();
691 let terminal = pty.terminal();
692 let term = terminal.lock();
693
694 let scrollback_len = term.active_grid().scrollback_len();
695 let mut results = Vec::new();
696
697 let scrollback_matches = term.search_scrollback(query, case_sensitive, None);
698 for m in scrollback_matches {
699 let abs_line = scrollback_len as isize + m.row;
700 if abs_line >= 0 {
701 results.push(crate::SearchMatch::new(abs_line as usize, m.col, m.length));
702 }
703 }
704
705 let screen_matches = term.search_text(query, case_sensitive);
706 for m in screen_matches {
707 let abs_line = scrollback_len + m.row as usize;
708 results.push(crate::SearchMatch::new(abs_line, m.col, m.length));
709 }
710
711 results.sort_by(|a, b| a.line.cmp(&b.line).then_with(|| a.column.cmp(&b.column)));
712
713 results
714 }
715
716 pub fn take_notifications(&self) -> Vec<par_term_emu_core_rust::terminal::Notification> {
718 let pty = self.pty_session.lock();
719 let terminal = pty.terminal();
720 let mut term = terminal.lock();
721 term.take_notifications()
722 }
723
724 pub fn has_notifications(&self) -> bool {
726 let pty = self.pty_session.lock();
727 let terminal = pty.terminal();
728 let term = terminal.lock();
729 term.has_notifications()
730 }
731
732 #[allow(dead_code)]
734 pub fn screenshot_to_file(
735 &self,
736 path: &std::path::Path,
737 format: &str,
738 scrollback_lines: usize,
739 ) -> Result<()> {
740 use par_term_emu_core_rust::screenshot::{ImageFormat, ScreenshotConfig};
741
742 log::info!(
743 "Taking screenshot to: {} (format: {}, scrollback: {})",
744 path.display(),
745 format,
746 scrollback_lines
747 );
748
749 let pty = self.pty_session.lock();
750 let terminal = pty.terminal();
751 let term = terminal.lock();
752
753 let image_format = match format.to_lowercase().as_str() {
754 "png" => ImageFormat::Png,
755 "jpeg" | "jpg" => ImageFormat::Jpeg,
756 "svg" => ImageFormat::Svg,
757 _ => {
758 log::warn!("Unknown format '{}', defaulting to PNG", format);
759 ImageFormat::Png
760 }
761 };
762
763 let config = ScreenshotConfig {
764 format: image_format,
765 ..Default::default()
766 };
767
768 term.screenshot_to_file(path, config, scrollback_lines)
769 .map_err(|e| anyhow::anyhow!("Failed to save screenshot: {}", e))?;
770
771 log::info!("Screenshot saved successfully");
772 Ok(())
773 }
774
775 pub fn record_marker(&self, label: String) {
777 log::debug!("Recording marker: {}", label);
778 let pty = self.pty_session.lock();
779 let terminal = pty.terminal();
780 let mut term = terminal.lock();
781 term.record_marker(label);
782 }
783
784 pub fn export_recording_to_file(
786 &self,
787 session: &par_term_emu_core_rust::terminal::RecordingSession,
788 path: &std::path::Path,
789 format: &str,
790 ) -> Result<()> {
791 log::info!("Exporting recording to {}: {}", format, path.display());
792 let pty = self.pty_session.lock();
793 let terminal = pty.terminal();
794 let term = terminal.lock();
795
796 let content = match format.to_lowercase().as_str() {
797 "json" => term.export_json(session),
798 _ => term.export_asciicast(session),
799 };
800
801 std::fs::write(path, content)?;
802 log::info!("Recording exported successfully");
803 Ok(())
804 }
805
806 pub fn shell_integration_cwd(&self) -> Option<String> {
808 let pty = self.pty_session.lock();
809 let terminal = pty.terminal();
810 let term = terminal.lock();
811 term.shell_integration().cwd().map(String::from)
812 }
813
814 pub fn shell_integration_exit_code(&self) -> Option<i32> {
816 let pty = self.pty_session.lock();
817 let terminal = pty.terminal();
818 let term = terminal.lock();
819 term.shell_integration().exit_code()
820 }
821
822 pub fn shell_integration_command(&self) -> Option<String> {
824 let pty = self.pty_session.lock();
825 let terminal = pty.terminal();
826 let term = terminal.lock();
827 term.shell_integration().command().map(String::from)
828 }
829
830 pub fn shell_integration_hostname(&self) -> Option<String> {
832 let pty = self.pty_session.lock();
833 let terminal = pty.terminal();
834 let term = terminal.lock();
835 term.shell_integration().hostname().map(String::from)
836 }
837
838 pub fn shell_integration_username(&self) -> Option<String> {
840 let pty = self.pty_session.lock();
841 let terminal = pty.terminal();
842 let term = terminal.lock();
843 term.shell_integration().username().map(String::from)
844 }
845
846 pub fn poll_cwd_events(&self) -> Vec<par_term_emu_core_rust::terminal::CwdChange> {
848 let pty = self.pty_session.lock();
849 let terminal = pty.terminal();
850 let mut term = terminal.lock();
851 term.poll_cwd_events()
852 }
853
854 pub fn poll_action_results(&self) -> Vec<par_term_emu_core_rust::terminal::ActionResult> {
856 let pty = self.pty_session.lock();
857 let terminal = pty.terminal();
858 let mut term = terminal.lock();
859 term.poll_action_results()
860 }
861
862 pub fn get_active_transfers(
865 &self,
866 ) -> Vec<par_term_emu_core_rust::terminal::file_transfer::FileTransfer> {
867 let pty = self.pty_session.lock();
868 let terminal = pty.terminal();
869 let term = terminal.lock();
870 term.get_active_transfers()
871 }
872
873 pub fn get_completed_transfers(
874 &self,
875 ) -> Vec<par_term_emu_core_rust::terminal::file_transfer::FileTransfer> {
876 let pty = self.pty_session.lock();
877 let terminal = pty.terminal();
878 let term = terminal.lock();
879 term.get_completed_transfers()
880 }
881
882 pub fn take_completed_transfer(
883 &self,
884 id: u64,
885 ) -> Option<par_term_emu_core_rust::terminal::file_transfer::FileTransfer> {
886 let pty = self.pty_session.lock();
887 let terminal = pty.terminal();
888 let mut term = terminal.lock();
889 term.take_completed_transfer(id)
890 }
891
892 pub fn cancel_file_transfer(&self, id: u64) -> bool {
893 let pty = self.pty_session.lock();
894 let terminal = pty.terminal();
895 let mut term = terminal.lock();
896 term.cancel_file_transfer(id)
897 }
898
899 pub fn send_upload_data(&self, data: &[u8]) {
900 let pty = self.pty_session.lock();
901 let terminal = pty.terminal();
902 let mut term = terminal.lock();
903 term.send_upload_data(data);
904 }
905
906 pub fn cancel_upload(&self) {
907 let pty = self.pty_session.lock();
908 let terminal = pty.terminal();
909 let mut term = terminal.lock();
910 term.cancel_upload();
911 }
912
913 pub fn poll_upload_requests(&self) -> Vec<String> {
914 let pty = self.pty_session.lock();
915 let terminal = pty.terminal();
916 let mut term = terminal.lock();
917 term.poll_upload_requests()
918 }
919
920 pub fn custom_session_variables(&self) -> std::collections::HashMap<String, String> {
921 let pty = self.pty_session.lock();
922 let terminal = pty.terminal();
923 let term = terminal.lock();
924 term.session_variables().custom.clone()
925 }
926
927 pub fn shell_integration_stats(
928 &self,
929 ) -> par_term_emu_core_rust::terminal::ShellIntegrationStats {
930 let pty = self.pty_session.lock();
931 let terminal = pty.terminal();
932 let term = terminal.lock();
933 term.get_shell_integration_stats()
934 }
935
936 #[allow(dead_code)]
938 pub fn cursor_position(&self) -> (usize, usize) {
939 let pty = self.pty_session.lock();
940 pty.cursor_position()
941 }
942
943 pub fn cursor_style(&self) -> par_term_emu_core_rust::cursor::CursorStyle {
945 let pty = self.pty_session.lock();
946 let terminal = pty.terminal();
947 let term = terminal.lock();
948 term.cursor().style()
949 }
950
951 pub fn set_cursor_style(&mut self, style: par_term_emu_core_rust::cursor::CursorStyle) {
953 let pty = self.pty_session.lock();
954 let terminal = pty.terminal();
955 let mut term = terminal.lock();
956 term.set_cursor_style(style);
957 }
958
959 pub fn is_cursor_visible(&self) -> bool {
961 let pty = self.pty_session.lock();
962 let terminal = pty.terminal();
963 let term = terminal.lock();
964 term.cursor().visible
965 }
966
967 pub fn is_mouse_tracking_enabled(&self) -> bool {
969 let pty = self.pty_session.lock();
970 let terminal = pty.terminal();
971 let term = terminal.lock();
972 !matches!(
973 term.mouse_mode(),
974 par_term_emu_core_rust::mouse::MouseMode::Off
975 )
976 }
977
978 pub fn report_focus_change(&self, focused: bool) -> bool {
981 let pty = self.pty_session.lock();
982 let terminal = pty.terminal();
983 let term = terminal.lock();
984 let data = if focused {
985 term.report_focus_in()
986 } else {
987 term.report_focus_out()
988 };
989 if !data.is_empty() {
990 drop(term);
991 drop(terminal);
992 drop(pty);
993 if let Err(e) = self.write(&data) {
995 log::error!("Failed to write focus event to PTY: {}", e);
996 return false;
997 }
998 true
999 } else {
1000 false
1001 }
1002 }
1003
1004 pub fn is_alt_screen_active(&self) -> bool {
1006 let pty = self.pty_session.lock();
1007 let terminal = pty.terminal();
1008 let term = terminal.lock();
1009 term.is_alt_screen_active()
1010 }
1011
1012 pub fn modify_other_keys_mode(&self) -> u8 {
1014 let pty = self.pty_session.lock();
1015 let terminal = pty.terminal();
1016 let term = terminal.lock();
1017 term.modify_other_keys_mode()
1018 }
1019
1020 pub fn application_cursor(&self) -> bool {
1022 let pty = self.pty_session.lock();
1023 let terminal = pty.terminal();
1024 let term = terminal.lock();
1025 term.application_cursor()
1026 }
1027
1028 pub fn get_title(&self) -> String {
1030 let pty = self.pty_session.lock();
1031 let terminal = pty.terminal();
1032 let term = terminal.lock();
1033 term.title().to_string()
1034 }
1035
1036 pub fn shell_integration_marker(
1038 &self,
1039 ) -> Option<par_term_emu_core_rust::shell_integration::ShellIntegrationMarker> {
1040 let pty = self.pty_session.lock();
1041 let terminal = pty.terminal();
1042 let term = terminal.lock();
1043 term.shell_integration().marker()
1044 }
1045
1046 pub fn is_command_running(&self) -> bool {
1048 use par_term_emu_core_rust::shell_integration::ShellIntegrationMarker;
1049
1050 matches!(
1051 self.shell_integration_marker(),
1052 Some(ShellIntegrationMarker::CommandExecuted)
1053 )
1054 }
1055
1056 pub fn get_running_command_name(&self) -> Option<String> {
1058 if !self.is_command_running() {
1059 return None;
1060 }
1061
1062 self.shell_integration_command().and_then(|cmd| {
1063 let first_word = cmd.split_whitespace().next()?;
1064 let name = std::path::Path::new(first_word)
1065 .file_name()
1066 .and_then(|n| n.to_str())
1067 .unwrap_or(first_word);
1068 Some(name.to_string())
1069 })
1070 }
1071
1072 pub fn should_confirm_close(&self, jobs_to_ignore: &[String]) -> Option<String> {
1074 let command_name = self.get_running_command_name()?;
1075
1076 let command_lower = command_name.to_lowercase();
1077 for ignore in jobs_to_ignore {
1078 if ignore.to_lowercase() == command_lower {
1079 return None;
1080 }
1081 }
1082
1083 Some(command_name)
1084 }
1085
1086 pub fn should_report_mouse_motion(&self, button_pressed: bool) -> bool {
1088 let pty = self.pty_session.lock();
1089 let terminal = pty.terminal();
1090 let term = terminal.lock();
1091
1092 match term.mouse_mode() {
1093 par_term_emu_core_rust::mouse::MouseMode::AnyEvent => true,
1094 par_term_emu_core_rust::mouse::MouseMode::ButtonEvent => button_pressed,
1095 _ => false,
1096 }
1097 }
1098
1099 pub fn encode_mouse_event(
1101 &self,
1102 button: u8,
1103 col: usize,
1104 row: usize,
1105 pressed: bool,
1106 modifiers: u8,
1107 ) -> Vec<u8> {
1108 let pty = self.pty_session.lock();
1109 let terminal = pty.terminal();
1110 let mut term = terminal.lock();
1111
1112 let mouse_event =
1113 par_term_emu_core_rust::mouse::MouseEvent::new(button, col, row, pressed, modifiers);
1114 term.report_mouse(mouse_event)
1115 }
1116
1117 #[allow(dead_code)]
1119 pub fn get_styled_segments(&self) -> Vec<StyledSegment> {
1120 let pty = self.pty_session.lock();
1121 let terminal = pty.terminal();
1122 let term = terminal.lock();
1123 let grid = term.active_grid();
1124 extract_styled_segments(grid)
1125 }
1126
1127 pub fn update_generation(&self) -> u64 {
1129 let pty = self.pty_session.lock();
1130 pty.update_generation()
1131 }
1132}
1133
1134impl TerminalManager {}
1139
1140impl TerminalManager {
1145 pub fn progress_bar(&self) -> par_term_emu_core_rust::terminal::ProgressBar {
1147 let pty = self.pty_session.lock();
1148 let terminal = pty.terminal();
1149 let term = terminal.lock();
1150 *term.progress_bar()
1151 }
1152
1153 pub fn named_progress_bars(
1155 &self,
1156 ) -> std::collections::HashMap<String, par_term_emu_core_rust::terminal::NamedProgressBar> {
1157 let pty = self.pty_session.lock();
1158 let terminal = pty.terminal();
1159 let term = terminal.lock();
1160 term.named_progress_bars().clone()
1161 }
1162
1163 pub fn has_any_progress(&self) -> bool {
1165 let pty = self.pty_session.lock();
1166 let terminal = pty.terminal();
1167 let term = terminal.lock();
1168 term.has_progress() || !term.named_progress_bars().is_empty()
1169 }
1170}
1171
1172impl TerminalManager {
1177 pub fn set_answerback_string(&self, answerback: Option<String>) {
1178 let pty = self.pty_session.lock();
1179 let terminal = pty.terminal();
1180 let mut term = terminal.lock();
1181 term.set_answerback_string(answerback);
1182 }
1183
1184 pub fn set_width_config(&self, config: par_term_emu_core_rust::WidthConfig) {
1185 let pty = self.pty_session.lock();
1186 let terminal = pty.terminal();
1187 let mut term = terminal.lock();
1188 term.set_width_config(config);
1189 }
1190
1191 pub fn set_normalization_form(&self, form: par_term_emu_core_rust::NormalizationForm) {
1192 let pty = self.pty_session.lock();
1193 let terminal = pty.terminal();
1194 let mut term = terminal.lock();
1195 term.set_normalization_form(form);
1196 }
1197
1198 pub fn set_output_callback<F>(&self, callback: F)
1199 where
1200 F: Fn(&[u8]) + Send + Sync + 'static,
1201 {
1202 let mut pty = self.pty_session.lock();
1203 pty.set_output_callback(std::sync::Arc::new(callback));
1204 }
1205
1206 pub fn start_recording(&self, title: Option<String>) {
1207 let pty = self.pty_session.lock();
1208 let terminal = pty.terminal();
1209 let mut term = terminal.lock();
1210 term.start_recording(title);
1211 }
1212
1213 pub fn stop_recording(&self) -> Option<par_term_emu_core_rust::terminal::RecordingSession> {
1214 let pty = self.pty_session.lock();
1215 let terminal = pty.terminal();
1216 let mut term = terminal.lock();
1217 term.stop_recording()
1218 }
1219
1220 pub fn is_recording(&self) -> bool {
1221 let pty = self.pty_session.lock();
1222 let terminal = pty.terminal();
1223 let term = terminal.lock();
1224 term.is_recording()
1225 }
1226
1227 pub fn export_asciicast(
1228 &self,
1229 session: &par_term_emu_core_rust::terminal::RecordingSession,
1230 ) -> String {
1231 let pty = self.pty_session.lock();
1232 let terminal = pty.terminal();
1233 let term = terminal.lock();
1234 term.export_asciicast(session)
1235 }
1236}
1237
1238impl TerminalManager {
1243 pub fn start_coprocess(
1244 &self,
1245 config: par_term_emu_core_rust::coprocess::CoprocessConfig,
1246 ) -> std::result::Result<par_term_emu_core_rust::coprocess::CoprocessId, String> {
1247 let pty = self.pty_session.lock();
1248 pty.start_coprocess(config)
1249 }
1250
1251 pub fn stop_coprocess(
1252 &self,
1253 id: par_term_emu_core_rust::coprocess::CoprocessId,
1254 ) -> std::result::Result<(), String> {
1255 let pty = self.pty_session.lock();
1256 pty.stop_coprocess(id)
1257 }
1258
1259 pub fn coprocess_status(
1260 &self,
1261 id: par_term_emu_core_rust::coprocess::CoprocessId,
1262 ) -> Option<bool> {
1263 let pty = self.pty_session.lock();
1264 pty.coprocess_status(id)
1265 }
1266
1267 pub fn read_from_coprocess(
1268 &self,
1269 id: par_term_emu_core_rust::coprocess::CoprocessId,
1270 ) -> std::result::Result<Vec<String>, String> {
1271 let pty = self.pty_session.lock();
1272 pty.read_from_coprocess(id)
1273 }
1274
1275 pub fn list_coprocesses(&self) -> Vec<par_term_emu_core_rust::coprocess::CoprocessId> {
1276 let pty = self.pty_session.lock();
1277 pty.list_coprocesses()
1278 }
1279
1280 pub fn read_coprocess_errors(
1281 &self,
1282 id: par_term_emu_core_rust::coprocess::CoprocessId,
1283 ) -> std::result::Result<Vec<String>, String> {
1284 let pty = self.pty_session.lock();
1285 pty.read_coprocess_errors(id)
1286 }
1287}
1288
1289impl TerminalManager {
1294 pub fn set_tmux_control_mode(&self, enabled: bool) {
1295 let pty = self.pty_session.lock();
1296 let terminal = pty.terminal();
1297 let mut term = terminal.lock();
1298 term.set_tmux_control_mode(enabled);
1299 }
1300
1301 pub fn is_tmux_control_mode(&self) -> bool {
1302 let pty = self.pty_session.lock();
1303 let terminal = pty.terminal();
1304 let term = terminal.lock();
1305 term.is_tmux_control_mode()
1306 }
1307
1308 pub fn drain_tmux_notifications(
1309 &self,
1310 ) -> Vec<par_term_emu_core_rust::tmux_control::TmuxNotification> {
1311 let pty = self.pty_session.lock();
1312 let terminal = pty.terminal();
1313 let mut term = terminal.lock();
1314 term.drain_tmux_notifications()
1315 }
1316
1317 pub fn tmux_notifications(
1318 &self,
1319 ) -> Vec<par_term_emu_core_rust::tmux_control::TmuxNotification> {
1320 let pty = self.pty_session.lock();
1321 let terminal = pty.terminal();
1322 let term = terminal.lock();
1323 term.tmux_notifications().to_vec()
1324 }
1325}
1326
1327impl TerminalManager {
1332 pub fn sync_triggers(&self, triggers: &[par_term_config::TriggerConfig]) {
1334 let pty = self.pty_session.lock();
1335 let terminal = pty.terminal();
1336 let mut term = terminal.lock();
1337
1338 let existing: Vec<u64> = term.list_triggers().iter().map(|t| t.id).collect();
1339 for id in existing {
1340 term.remove_trigger(id);
1341 }
1342
1343 for trigger_config in triggers {
1344 let actions: Vec<par_term_emu_core_rust::terminal::TriggerAction> = trigger_config
1345 .actions
1346 .iter()
1347 .map(|a| a.to_core_action())
1348 .collect();
1349
1350 match term.add_trigger(
1351 trigger_config.name.clone(),
1352 trigger_config.pattern.clone(),
1353 actions,
1354 ) {
1355 Ok(id) => {
1356 if !trigger_config.enabled {
1357 term.set_trigger_enabled(id, false);
1358 }
1359 log::info!("Trigger '{}' registered (id={})", trigger_config.name, id);
1360 }
1361 Err(e) => {
1362 log::error!(
1363 "Failed to register trigger '{}': {}",
1364 trigger_config.name,
1365 e
1366 );
1367 }
1368 }
1369 }
1370 }
1371}
1372
1373impl TerminalManager {
1378 pub fn add_observer(
1379 &self,
1380 observer: std::sync::Arc<dyn par_term_emu_core_rust::observer::TerminalObserver>,
1381 ) -> par_term_emu_core_rust::observer::ObserverId {
1382 let pty = self.pty_session.lock();
1383 let terminal = pty.terminal();
1384 let mut term = terminal.lock();
1385 term.add_observer(observer)
1386 }
1387
1388 pub fn remove_observer(&self, id: par_term_emu_core_rust::observer::ObserverId) -> bool {
1389 let pty = self.pty_session.lock();
1390 let terminal = pty.terminal();
1391 let mut term = terminal.lock();
1392 term.remove_observer(id)
1393 }
1394}
1395
1396impl Drop for TerminalManager {
1397 fn drop(&mut self) {
1398 log::info!("Shutting down terminal manager");
1399
1400 if let Some(mut pty) = self.pty_session.try_lock() {
1401 if pty.is_running() {
1402 log::info!("Killing PTY process during shutdown");
1403 if let Err(e) = pty.kill() {
1404 log::warn!("Failed to kill PTY process: {:?}", e);
1405 }
1406 }
1407 } else {
1408 log::warn!("Could not acquire PTY lock during terminal manager shutdown");
1409 }
1410
1411 log::info!("Terminal manager shutdown complete");
1412 }
1413}