Skip to main content

par_term_terminal/terminal/
mod.rs

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
11// Re-export clipboard types for use in other modules
12pub 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
20/// Resolve the user's login shell PATH and return environment variables for coprocess spawning.
21///
22/// On macOS (and other Unix), app bundles have a minimal PATH that doesn't include
23/// user-installed paths like `/opt/homebrew/bin`, `/usr/local/bin`, etc.
24/// This function runs the user's login shell once to resolve the full PATH,
25/// caches the result, and returns it as a HashMap suitable for `CoprocessConfig.env`.
26pub 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
74/// Terminal manager that wraps the PTY session
75pub struct TerminalManager {
76    /// The underlying PTY session
77    pub(crate) pty_session: Arc<Mutex<PtySession>>,
78    /// Terminal dimensions (cols, rows)
79    pub(crate) dimensions: (usize, usize),
80    /// Color theme for ANSI colors
81    pub(crate) theme: Theme,
82    /// Scrollback metadata for shell integration markers
83    pub(crate) scrollback_metadata: ScrollbackMetadata,
84    /// Previous shell integration marker for detecting transitions
85    last_shell_marker: Option<ShellIntegrationMarker>,
86    /// Absolute line and column at CommandStart (B marker) for extracting command text.
87    /// Stored as (absolute_line, col) where absolute_line = scrollback_len + cursor_row
88    /// at the time the B marker was seen. Using absolute line rather than grid row
89    /// ensures we can still find the command text even after it scrolls into scrollback.
90    command_start_pos: Option<(usize, usize)>,
91    /// Command text captured from the terminal (waiting to be applied to a mark).
92    /// Stored as (absolute_line, text) so we can target the correct mark.
93    captured_command_text: Option<(usize, String)>,
94}
95
96impl TerminalManager {
97    /// Create a new terminal manager with the specified dimensions
98    #[allow(dead_code)]
99    pub fn new(cols: usize, rows: usize) -> Result<Self> {
100        Self::new_with_scrollback(cols, rows, 10000)
101    }
102
103    /// Create a new terminal manager with specified dimensions and scrollback size
104    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    /// Set the color theme
127    pub fn set_theme(&mut self, theme: Theme) {
128        self.theme = theme;
129    }
130
131    /// Update scrollback metadata based on shell integration events from the core.
132    ///
133    /// Drains the queued `ShellIntegrationEvent` events from the terminal, each of
134    /// which carries the `cursor_line` at the exact moment the OSC 133 marker was
135    /// parsed. This eliminates the batching problem where multiple markers arrive
136    /// between frames and only the last one was visible via `marker()`.
137    ///
138    /// Command text is captured from the terminal grid when the marker changes
139    /// away from CommandStart, then injected into scrollback marks after
140    /// `apply_event()` creates them.
141    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        // Drain queued shell integration events with their recorded cursor positions.
147        let shell_events = term.poll_shell_integration_events();
148
149        // Read cumulative history state (used only for CommandFinished events).
150        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        // Process each queued event at its recorded cursor position.
157        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                // Track cursor position at CommandStart (B) for command text extraction.
170                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                // Only pass history/exit info for CommandFinished events.
196                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                // Feed command lifecycle into the core library's command history.
211                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 we captured command text, apply it to the mark at the command's
234        // absolute line.
235        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    /// Extract command text from the terminal using absolute line positioning.
241    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    /// Read text from a scrollback line, converting cells to a string.
281    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    /// Get rendered scrollback marks (prompt/command boundaries).
298    pub fn scrollback_marks(&self) -> Vec<ScrollbackMark> {
299        self.scrollback_metadata.marks()
300    }
301
302    /// Find previous prompt mark before the given absolute line (if any).
303    pub fn scrollback_previous_mark(&self, line: usize) -> Option<usize> {
304        self.scrollback_metadata.previous_mark(line)
305    }
306
307    /// Find next prompt mark after the given absolute line (if any).
308    pub fn scrollback_next_mark(&self, line: usize) -> Option<usize> {
309        self.scrollback_metadata.next_mark(line)
310    }
311
312    /// Get command history from the core library (commands tracked via shell integration).
313    ///
314    /// Returns commands as `(command_text, exit_code, duration_ms)` tuples.
315    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    /// Set cell dimensions in pixels for sixel graphics scroll calculations
326    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    /// Write data to the PTY (send user input to shell)
334    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    /// Write string to the PTY
349    #[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    /// Process raw data through the terminal emulator (for tmux output routing).
358    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    /// Paste text to the terminal with proper bracketed paste handling.
366    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    /// Paste text with a delay between lines.
403    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    /// Get the terminal content as a string
461    #[allow(dead_code)]
462    pub fn content(&self) -> Result<String> {
463        let pty = self.pty_session.lock();
464        Ok(pty.content())
465    }
466
467    /// Resize the terminal
468    #[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    /// Resize the terminal with pixel dimensions
481    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    /// Set pixel dimensions for XTWINOPS CSI 14 t query support
505    #[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    /// Get the current terminal dimensions
515    #[allow(dead_code)]
516    pub fn dimensions(&self) -> (usize, usize) {
517        self.dimensions
518    }
519
520    /// Get a clone of the underlying terminal for direct access
521    #[allow(dead_code)]
522    pub fn terminal(&self) -> Arc<Mutex<Terminal>> {
523        let pty = self.pty_session.lock();
524        pty.terminal()
525    }
526
527    /// Check if there have been updates since last check
528    #[allow(dead_code)]
529    pub fn has_updates(&self) -> bool {
530        false
531    }
532
533    /// Check if the PTY is still running
534    pub fn is_running(&self) -> bool {
535        let pty = self.pty_session.lock();
536        pty.is_running()
537    }
538
539    /// Kill the PTY process
540    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    /// Get the current bell event count
547    pub fn bell_count(&self) -> u64 {
548        let pty = self.pty_session.lock();
549        pty.bell_count()
550    }
551
552    /// Get scrollback lines
553    #[allow(dead_code)]
554    pub fn scrollback(&self) -> Vec<String> {
555        let pty = self.pty_session.lock();
556        pty.scrollback()
557    }
558
559    /// Get scrollback length
560    pub fn scrollback_len(&self) -> usize {
561        let pty = self.pty_session.lock();
562        pty.scrollback_len()
563    }
564
565    /// Get text of a line at an absolute index (scrollback + screen).
566    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    /// Get all lines in a range as text (for search in copy mode).
586    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    /// Get all scrollback lines as Cell arrays.
615    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,     // screen_row (unused for our purposes)
633                    None,  // no selection
634                    false, // not rectangular
635                    None,  // no cursor
636                    &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    /// Clear scrollback buffer
648    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    /// Clear scrollback metadata (prompt marks, command history, timestamps).
656    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    /// Search for text in the visible screen.
664    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    /// Search for text in the scrollback buffer.
676    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    /// Search for text in both visible screen and scrollback.
689    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    /// Take all pending OSC 9/777 notifications
717    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    /// Check if there are pending OSC 9/777 notifications
725    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    /// Take a screenshot of the terminal and save to file
733    #[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    /// Add a marker to the recording
776    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    /// Export recording to file (asciicast or JSON format)
785    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    /// Get current working directory from shell integration (OSC 7)
807    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    /// Get last command exit code from shell integration (OSC 133)
815    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    /// Get current command from shell integration
823    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    /// Get hostname from shell integration (OSC 7)
831    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    /// Get username from shell integration (OSC 7)
839    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    /// Poll CWD change events from terminal
847    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    /// Poll trigger action results from the core terminal.
855    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    // === File Transfer Methods ===
863
864    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    /// Get cursor position
937    #[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    /// Get cursor style from terminal for rendering
944    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    /// Set cursor style for the terminal
952    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    /// Check if cursor is visible
960    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    /// Check if mouse tracking is enabled
968    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    /// Send focus event to PTY if the application has enabled focus tracking (DECSET 1004).
979    /// Returns true if the event was sent.
980    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            // Write the focus event sequence to PTY
994            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    /// Check if alternate screen is active
1005    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    /// Get the modifyOtherKeys mode
1013    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    /// Check if application cursor key mode (DECCKM) is enabled.
1021    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    /// Get the terminal title set by OSC 0, 1, or 2 sequences
1029    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    /// Get the current shell integration marker state
1037    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    /// Check if a command is currently running based on shell integration
1047    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    /// Get the name of the currently running command (first word only)
1057    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    /// Check if tab close should show a confirmation dialog
1073    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    /// Check if mouse motion events should be reported
1087    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    /// Send a mouse event to the terminal and get the encoded bytes
1100    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    /// Get styled segments from the terminal for rendering
1118    #[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    /// Get the current generation number for dirty tracking
1128    pub fn update_generation(&self) -> u64 {
1129        let pty = self.pty_session.lock();
1130        pty.update_generation()
1131    }
1132}
1133
1134// ========================================================================
1135// Clipboard History Methods
1136// ========================================================================
1137
1138impl TerminalManager {}
1139
1140// ========================================================================
1141// Progress Bar Methods (OSC 9;4 and OSC 934)
1142// ========================================================================
1143
1144impl TerminalManager {
1145    /// Get the simple progress bar state (OSC 9;4)
1146    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    /// Get all named progress bars (OSC 934)
1154    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    /// Check if any progress bar is currently active
1164    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
1172// ========================================================================
1173// Answerback String (ENQ Response)
1174// ========================================================================
1175
1176impl 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
1238// ========================================================================
1239// Coprocess Management Methods
1240// ========================================================================
1241
1242impl 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
1289// ========================================================================
1290// tmux Control Mode Methods
1291// ========================================================================
1292
1293impl 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
1327// ========================================================================
1328// Trigger Sync Methods
1329// ========================================================================
1330
1331impl TerminalManager {
1332    /// Sync trigger configs from Config into the core TriggerRegistry.
1333    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
1373// ========================================================================
1374// Observer Management Methods
1375// ========================================================================
1376
1377impl 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}