par_term/
terminal.rs

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
10// Re-export clipboard types for use in other modules
11pub use par_term_emu_core_rust::terminal::{ClipboardEntry, ClipboardSlot};
12
13/// Convert ANSI color index to RGB
14#[allow(dead_code)]
15fn ansi_to_rgb(color_idx: u8) -> [u8; 3] {
16    match color_idx {
17        // Standard 16 colors
18        0 => [0, 0, 0],        // Black
19        1 => [205, 0, 0],      // Red
20        2 => [0, 205, 0],      // Green
21        3 => [205, 205, 0],    // Yellow
22        4 => [0, 0, 238],      // Blue
23        5 => [205, 0, 205],    // Magenta
24        6 => [0, 205, 205],    // Cyan
25        7 => [229, 229, 229],  // White
26        8 => [127, 127, 127],  // Bright Black (Gray)
27        9 => [255, 0, 0],      // Bright Red
28        10 => [0, 255, 0],     // Bright Green
29        11 => [255, 255, 0],   // Bright Yellow
30        12 => [92, 92, 255],   // Bright Blue
31        13 => [255, 0, 255],   // Bright Magenta
32        14 => [0, 255, 255],   // Bright Cyan
33        15 => [255, 255, 255], // Bright White
34        // 216 color cube (16-231)
35        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        // Grayscale (232-255)
43        232..=255 => {
44            let gray = 8 + (color_idx - 232) * 10;
45            [gray, gray, gray]
46        }
47    }
48}
49
50/// Terminal manager that wraps the PTY session
51pub struct TerminalManager {
52    /// The underlying PTY session
53    pty_session: Arc<Mutex<PtySession>>,
54    /// Terminal dimensions (cols, rows)
55    dimensions: (usize, usize),
56    /// Color theme for ANSI colors
57    theme: Theme,
58}
59
60impl TerminalManager {
61    /// Create a new terminal manager with the specified dimensions
62    #[allow(dead_code)]
63    pub fn new(cols: usize, rows: usize) -> Result<Self> {
64        Self::new_with_scrollback(cols, rows, 10000)
65    }
66
67    /// Create a new terminal manager with specified dimensions and scrollback size
68    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    /// Set the color theme
87    pub fn set_theme(&mut self, theme: Theme) {
88        self.theme = theme;
89    }
90
91    /// Set cell dimensions in pixels for sixel graphics scroll calculations
92    ///
93    /// This should be called when the renderer is initialized or cell size changes.
94    /// Default is (1, 2) for TUI half-block rendering.
95    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    /// Spawn a shell in the terminal
103    #[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    /// Spawn a custom shell command in the terminal
113    ///
114    /// # Arguments
115    /// * `command` - The shell command to execute (e.g., "/bin/zsh", "fish")
116    #[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    /// Spawn a custom shell with arguments
127    ///
128    /// # Arguments
129    /// * `command` - The shell command to execute
130    /// * `args` - Arguments to pass to the shell
131    #[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    /// Spawn shell with optional working directory and environment variables
142    ///
143    /// # Arguments
144    /// * `working_dir` - Optional working directory path
145    /// * `env_vars` - Optional environment variables to set
146    #[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    /// Spawn custom shell with args, optional working directory, and environment variables
163    ///
164    /// # Arguments
165    /// * `command` - The shell command to execute
166    /// * `args` - Arguments to pass to the shell
167    /// * `working_dir` - Optional working directory path
168    /// * `env_vars` - Optional environment variables to set
169    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    /// Write data to the PTY (send user input to shell)
193    pub fn write(&self, data: &[u8]) -> Result<()> {
194        // Debug log to track what we're sending
195        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    /// Write string to the PTY
209    #[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    /// Get the terminal content as a string
218    #[allow(dead_code)]
219    pub fn content(&self) -> Result<String> {
220        let pty = self.pty_session.lock();
221        Ok(pty.content())
222    }
223
224    /// Resize the terminal
225    #[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    /// Resize the terminal with pixel dimensions
238    /// This sets both the character dimensions AND pixel dimensions in the PTY winsize struct,
239    /// which is required for applications like kitty icat that query pixel dimensions via TIOCGWINSZ
240    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    /// Set pixel dimensions for XTWINOPS CSI 14 t query support
264    #[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    /// Get the current terminal dimensions
274    #[allow(dead_code)]
275    pub fn dimensions(&self) -> (usize, usize) {
276        self.dimensions
277    }
278
279    /// Get a clone of the underlying terminal for direct access
280    #[allow(dead_code)]
281    pub fn terminal(&self) -> Arc<Mutex<Terminal>> {
282        let pty = self.pty_session.lock();
283        pty.terminal()
284    }
285
286    /// Check if there have been updates since last check
287    #[allow(dead_code)]
288    pub fn has_updates(&self) -> bool {
289        // For now, always assume there are updates since we poll at 60fps
290        // In the future, we could track update generation to optimize
291        true
292    }
293
294    /// Check if the PTY is still running
295    pub fn is_running(&self) -> bool {
296        let pty = self.pty_session.lock();
297        pty.is_running()
298    }
299
300    /// Kill the PTY process
301    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    /// Get the current bell event count
308    pub fn bell_count(&self) -> u64 {
309        let pty = self.pty_session.lock();
310        pty.bell_count()
311    }
312
313    /// Get scrollback lines
314    #[allow(dead_code)]
315    pub fn scrollback(&self) -> Vec<String> {
316        let pty = self.pty_session.lock();
317        pty.scrollback()
318    }
319
320    /// Get scrollback length
321    pub fn scrollback_len(&self) -> usize {
322        let pty = self.pty_session.lock();
323        pty.scrollback_len()
324    }
325
326    /// Clear scrollback buffer
327    ///
328    /// Removes all scrollback history while preserving the current screen content.
329    /// Uses CSI 3 J (ED 3) escape sequence which is the standard way to clear scrollback.
330    pub fn clear_scrollback(&self) {
331        let pty = self.pty_session.lock();
332        let terminal = pty.terminal();
333        let mut term = terminal.lock();
334        // CSI 3 J = ESC [ 3 J - Erase Scrollback (ED 3)
335        term.process(b"\x1b[3J");
336    }
337
338    /// Take all pending OSC 9/777 notifications
339    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    /// Check if there are pending OSC 9/777 notifications
347    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    /// Take a screenshot of the terminal and save to file
355    ///
356    /// # Arguments
357    /// * `path` - Path to save the screenshot
358    /// * `format` - Screenshot format ("png", "jpeg", "svg", "html")
359    /// * `scrollback_lines` - Number of scrollback lines to include (0 for none)
360    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        // Map format string to ImageFormat enum
380        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        // Create screenshot config
391        let config = ScreenshotConfig {
392            format: image_format,
393            ..Default::default()
394        };
395
396        // Call the core library's screenshot method
397        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    // TODO: Recording APIs not yet available in par-term-emu-core-rust
405    // Uncomment when the core library supports recording again
406
407    /*
408    /// Start recording a terminal session
409    pub fn start_recording(&self, title: Option<String>) {
410        log::info!("Starting session recording");
411        let pty = self.pty_session.lock();
412        let terminal = pty.terminal();
413        let mut term = terminal.lock();
414        term.start_recording(title);
415    }
416
417    /// Stop recording and return the recording session
418    pub fn stop_recording(&self) -> Option<par_term_emu_core_rust::terminal::RecordingSession> {
419        log::info!("Stopping session recording");
420        let pty = self.pty_session.lock();
421        let terminal = pty.terminal();
422        let mut term = terminal.lock();
423        term.stop_recording()
424    }
425
426    /// Add a marker to the recording
427    pub fn record_marker(&self, label: String) {
428        log::debug!("Recording marker: {}", label);
429        let pty = self.pty_session.lock();
430        let terminal = pty.terminal();
431        let mut term = terminal.lock();
432        term.record_marker(label);
433    }
434
435    /// Export recording to file (asciicast or JSON format)
436    pub fn export_recording_to_file(
437        &self,
438        session: &par_term_emu_core_rust::terminal::RecordingSession,
439        path: &std::path::Path,
440        format: &str,
441    ) -> Result<()> {
442        log::info!("Exporting recording to {}: {}", format, path.display());
443        let pty = self.pty_session.lock();
444        let terminal = pty.terminal();
445        let term = terminal.lock();
446
447        let content = match format.to_lowercase().as_str() {
448            "json" => term.export_json(session),
449            _ => term.export_asciicast(session), // default to asciicast
450        };
451
452        std::fs::write(path, content)?;
453        log::info!("Recording exported successfully");
454        Ok(())
455    }
456
457    /// Check if currently recording
458    pub fn is_recording(&self) -> bool {
459        let pty = self.pty_session.lock();
460        let terminal = pty.terminal();
461        let term = terminal.lock();
462        term.is_recording()
463    }
464    */
465
466    /// Get current working directory from shell integration (OSC 7)
467    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    /// Get last command exit code from shell integration (OSC 133)
475    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    /// Get current command from shell integration
483    #[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    // TODO: Shell integration stats API not yet available in par-term-emu-core-rust
492    /*
493    /// Get shell integration statistics
494    pub fn shell_integration_stats(
495        &self,
496    ) -> par_term_emu_core_rust::terminal::ShellIntegrationStats {
497        let pty = self.pty_session.lock();
498        let terminal = pty.terminal();
499        let term = terminal.lock();
500        term.get_shell_integration_stats()
501    }
502    */
503
504    /// Get all graphics (Sixel, iTerm2, Kitty)
505    /// Returns a vector of cloned TerminalGraphic objects for rendering
506    #[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    /// Get graphics at a specific row
535    #[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    /// Get total graphics count
550    #[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    /// Get all OSC 8 hyperlinks from the terminal
559    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    /// Get the URL for a specific hyperlink ID
567    #[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    /// Get all scrollback graphics
576    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    /// Update animations and return true if any frames changed
586    ///
587    /// This should be called periodically (e.g., in the redraw loop) to advance
588    /// animation frames based on timing. Returns true if any animation advanced
589    /// to a new frame, indicating that a redraw is needed.
590    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    /// Get all graphics with current animation frames
599    ///
600    /// For animated graphics, returns the current frame based on animation state.
601    /// For static graphics, returns the original graphic unchanged.
602    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        // First, collect all base graphics
612        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        // Then, for each graphic, check if it has an animation and get current frame
621        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            // Check if this graphic has an active animation
634            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                // Create a graphic from the current animation frame
639                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            // Not animated or no current frame - use original graphic
655            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    /// Get cursor position
664    #[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    /// Get cursor style from terminal for rendering
671    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    /// Check if cursor is visible (controlled by DECTCEM escape sequence)
679    ///
680    /// TUI applications typically hide the cursor when entering alternate screen mode.
681    /// Returns false when the terminal has received CSI ?25l (hide cursor).
682    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    /// Check if mouse tracking is enabled
690    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    /// Check if alternate screen is active (used by TUI applications)
701    ///
702    /// When the alternate screen is active, text selection should typically be disabled
703    /// as the content is controlled by an application (vim, htop, etc.) rather than
704    /// being scrollback history.
705    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    /// Get the terminal title set by OSC 0, 1, or 2 sequences
713    ///
714    /// Returns the title string that applications have set via escape sequences.
715    /// Returns empty string if no title has been set.
716    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    /// Check if mouse motion events should be reported
724    /// Returns true if mode is ButtonEvent or AnyEvent
725    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    /// Send a mouse event to the terminal and get the encoded bytes
738    ///
739    /// # Arguments
740    /// * `button` - Mouse button (0 = left, 1 = middle, 2 = right)
741    /// * `col` - Column position (0-indexed)
742    /// * `row` - Row position (0-indexed)
743    /// * `pressed` - true for press, false for release
744    /// * `modifiers` - Modifier keys bit mask
745    ///
746    /// # Returns
747    /// Encoded mouse event bytes to send to PTY, or empty vec if tracking is disabled
748    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    /// Get styled segments from the terminal for rendering
766    #[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    /// Get the current generation number for dirty tracking
776    ///
777    /// The generation number increments whenever the terminal content changes.
778    /// This can be used to detect when a cached representation needs to be updated.
779    pub fn update_generation(&self) -> u64 {
780        let pty = self.pty_session.lock();
781        pty.update_generation()
782    }
783
784    /// Get terminal grid with scrollback offset as Cell array for CellRenderer
785    ///
786    /// # Arguments
787    /// * `scroll_offset` - Number of lines to scroll back (0 = current view at bottom)
788    /// * `selection` - Optional selection range (start_col, start_row, end_col, end_row) in screen coordinates
789    /// * `rectangular` - Whether the selection is rectangular/block mode (default: false)
790    /// * `cursor` - Optional cursor (position, opacity) for smooth fade animations
791    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        // Don't pass cursor to cells - we'll render it separately as geometry
804        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    /// Check if a cell at (col, row) is within the selection range
938    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                // Rectangular selection: select cells within the column and row bounds
947                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            // Normal line-based selection
956            // Single line selection
957            if start_row == end_row {
958                return row == start_row && col >= start_col && col <= end_col;
959            }
960
961            // Multi-line selection
962            if row == start_row {
963                // First line - from start_col to end of line
964                return col >= start_col;
965            } else if row == end_row {
966                // Last line - from start of line to end_col
967                return col <= end_col;
968            } else if row > start_row && row < end_row {
969                // Middle lines - entire line selected
970                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        // Debug: Log cells with non-default backgrounds OR reverse flag (likely status bar)
986        // This helps diagnose TMUX status bar background rendering issues
987        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); // Not black background
990        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        // Apply theme colors for ANSI colors (Named colors)
1016        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, // Other colors default to foreground
1037                };
1038                (theme_color.r, theme_color.g, theme_color.b)
1039            }
1040            _ => term_cell.fg.to_rgb(), // Keep 256-color and RGB as-is
1041        };
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, // Other colors default to background
1064                };
1065                (theme_color.r, theme_color.g, theme_color.b)
1066            }
1067            _ => term_cell.bg.to_rgb(), // Keep 256-color and RGB as-is
1068        };
1069
1070        // Check if cell has reverse video flag (SGR 7) - TMUX uses this for status bar
1071        let is_reverse = term_cell.flags.reverse();
1072
1073        // Blend colors for smooth cursor fade animation, or invert for selection/reverse
1074        let (fg_color, bg_color) = if let Some((opacity, style)) = cursor_info {
1075            // Smooth cursor: blend between normal and inverted colors based on opacity and style
1076            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            // Different cursor styles - for now, all use inversion
1081            // TODO: Implement proper geometric rendering for beam/underline cursors
1082            // This requires adding cursor geometry to the cell renderer
1083            match style {
1084                // Block cursor: full inversion (default behavior)
1085                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                // Beam and Underline: Use same inversion for now
1100                // Proper implementation would draw thin lines in the renderer
1101                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            // Selection or Reverse video (SGR 7): invert colors
1121            (
1122                [bg.0, bg.1, bg.2, 255], // Swap: background becomes foreground
1123                [fg.0, fg.1, fg.2, 255], // Swap: foreground becomes background
1124            )
1125        } else {
1126            // Normal cell
1127            ([fg.0, fg.1, fg.2, 255], [bg.0, bg.1, bg.2, 255])
1128        };
1129
1130        // Optimization: Avoid String allocation for cells without combining chars
1131        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
1152// ========================================================================
1153// Clipboard History Methods
1154// ========================================================================
1155
1156impl TerminalManager {
1157    /// Get clipboard history for a specific slot
1158    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    /// Get the most recent clipboard entry for a slot
1166    #[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    /// Search clipboard history across all slots or a specific slot
1176    #[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    /// Add content to clipboard history
1190    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    /// Clear clipboard history for a specific slot
1203    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    /// Clear all clipboard history
1211    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    /// Set maximum clipboard sync events retained
1219    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    /// Set maximum bytes cached per clipboard event
1227    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    /// Set maximum clipboard history entries per slot
1235    #[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        // Explicitly clean up PTY session to ensure proper shutdown
1250        if let Some(mut pty) = self.pty_session.try_lock() {
1251            // Kill any running process
1252            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}