par_term/terminal/
mod.rs

1use crate::styled_content::{StyledSegment, extract_styled_segments};
2use crate::themes::Theme;
3use anyhow::Result;
4use par_term_emu_core_rust::pty_session::PtySession;
5use par_term_emu_core_rust::terminal::Terminal;
6use parking_lot::Mutex;
7use std::sync::Arc;
8
9// Re-export clipboard types for use in other modules
10pub use par_term_emu_core_rust::terminal::{ClipboardEntry, ClipboardSlot};
11
12/// Convert ANSI color index to RGB
13#[allow(dead_code)]
14fn ansi_to_rgb(color_idx: u8) -> [u8; 3] {
15    match color_idx {
16        // Standard 16 colors
17        0 => [0, 0, 0],        // Black
18        1 => [205, 0, 0],      // Red
19        2 => [0, 205, 0],      // Green
20        3 => [205, 205, 0],    // Yellow
21        4 => [0, 0, 238],      // Blue
22        5 => [205, 0, 205],    // Magenta
23        6 => [0, 205, 205],    // Cyan
24        7 => [229, 229, 229],  // White
25        8 => [127, 127, 127],  // Bright Black (Gray)
26        9 => [255, 0, 0],      // Bright Red
27        10 => [0, 255, 0],     // Bright Green
28        11 => [255, 255, 0],   // Bright Yellow
29        12 => [92, 92, 255],   // Bright Blue
30        13 => [255, 0, 255],   // Bright Magenta
31        14 => [0, 255, 255],   // Bright Cyan
32        15 => [255, 255, 255], // Bright White
33        // 216 color cube (16-231)
34        16..=231 => {
35            let idx = color_idx - 16;
36            let r = (idx / 36) * 51;
37            let g = ((idx % 36) / 6) * 51;
38            let b = (idx % 6) * 51;
39            [r, g, b]
40        }
41        // Grayscale (232-255)
42        232..=255 => {
43            let gray = 8 + (color_idx - 232) * 10;
44            [gray, gray, gray]
45        }
46    }
47}
48
49pub mod clipboard;
50pub mod graphics;
51pub mod hyperlinks;
52pub mod rendering;
53pub mod spawn;
54
55/// Terminal manager that wraps the PTY session
56pub struct TerminalManager {
57    /// The underlying PTY session
58    pub(crate) pty_session: Arc<Mutex<PtySession>>,
59    /// Terminal dimensions (cols, rows)
60    pub(crate) dimensions: (usize, usize),
61    /// Color theme for ANSI colors
62    pub(crate) theme: Theme,
63}
64
65impl TerminalManager {
66    /// Create a new terminal manager with the specified dimensions
67    #[allow(dead_code)]
68    pub fn new(cols: usize, rows: usize) -> Result<Self> {
69        Self::new_with_scrollback(cols, rows, 10000)
70    }
71
72    /// Create a new terminal manager with specified dimensions and scrollback size
73    pub fn new_with_scrollback(cols: usize, rows: usize, scrollback_size: usize) -> Result<Self> {
74        log::info!(
75            "Creating terminal with dimensions: {}x{}, scrollback: {}",
76            cols,
77            rows,
78            scrollback_size
79        );
80
81        let pty_session = PtySession::new(cols, rows, scrollback_size);
82        let pty_session = Arc::new(Mutex::new(pty_session));
83
84        Ok(Self {
85            pty_session,
86            dimensions: (cols, rows),
87            theme: Theme::default(),
88        })
89    }
90
91    /// Set the color theme
92    pub fn set_theme(&mut self, theme: Theme) {
93        self.theme = theme;
94    }
95
96    /// Set cell dimensions in pixels for sixel graphics scroll calculations
97    ///
98    /// This should be called when the renderer is initialized or cell size changes.
99    /// Default is (1, 2) for TUI half-block rendering.
100    pub fn set_cell_dimensions(&self, width: u32, height: u32) {
101        let pty = self.pty_session.lock();
102        let terminal = pty.terminal();
103        let mut term = terminal.lock();
104        term.set_cell_dimensions(width, height);
105    }
106
107    /// Write data to the PTY (send user input to shell)
108    pub fn write(&self, data: &[u8]) -> Result<()> {
109        // Debug log to track what we're sending
110        if !data.is_empty() {
111            log::debug!(
112                "Writing to PTY: {:?} (bytes: {:?})",
113                String::from_utf8_lossy(data),
114                data
115            );
116        }
117        let mut pty = self.pty_session.lock();
118        pty.write(data)
119            .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
120        Ok(())
121    }
122
123    /// Write string to the PTY
124    #[allow(dead_code)]
125    pub fn write_str(&self, data: &str) -> Result<()> {
126        let mut pty = self.pty_session.lock();
127        pty.write_str(data)
128            .map_err(|e| anyhow::anyhow!("Failed to write to PTY: {}", e))?;
129        Ok(())
130    }
131
132    /// Get the terminal content as a string
133    #[allow(dead_code)]
134    pub fn content(&self) -> Result<String> {
135        let pty = self.pty_session.lock();
136        Ok(pty.content())
137    }
138
139    /// Resize the terminal
140    #[allow(dead_code)]
141    pub fn resize(&mut self, cols: usize, rows: usize) -> Result<()> {
142        log::info!("Resizing terminal to: {}x{}", cols, rows);
143
144        let mut pty = self.pty_session.lock();
145        pty.resize(cols as u16, rows as u16)
146            .map_err(|e| anyhow::anyhow!("Failed to resize PTY: {}", e))?;
147
148        self.dimensions = (cols, rows);
149        Ok(())
150    }
151
152    /// Resize the terminal with pixel dimensions
153    /// This sets both the character dimensions AND pixel dimensions in the PTY winsize struct,
154    /// which is required for applications like kitty icat that query pixel dimensions via TIOCGWINSZ
155    pub fn resize_with_pixels(
156        &mut self,
157        cols: usize,
158        rows: usize,
159        width_px: usize,
160        height_px: usize,
161    ) -> Result<()> {
162        log::info!(
163            "Resizing terminal to: {}x{} ({}x{} pixels)",
164            cols,
165            rows,
166            width_px,
167            height_px
168        );
169
170        let mut pty = self.pty_session.lock();
171        pty.resize_with_pixels(cols as u16, rows as u16, width_px as u16, height_px as u16)
172            .map_err(|e| anyhow::anyhow!("Failed to resize PTY with pixels: {}", e))?;
173
174        self.dimensions = (cols, rows);
175        Ok(())
176    }
177
178    /// Set pixel dimensions for XTWINOPS CSI 14 t query support
179    #[allow(dead_code)]
180    pub fn set_pixel_size(&mut self, width_px: usize, height_px: usize) -> Result<()> {
181        let pty = self.pty_session.lock();
182        let term_arc = pty.terminal();
183        let mut term = term_arc.lock();
184        term.set_pixel_size(width_px, height_px);
185        Ok(())
186    }
187
188    /// Get the current terminal dimensions
189    #[allow(dead_code)]
190    pub fn dimensions(&self) -> (usize, usize) {
191        self.dimensions
192    }
193
194    /// Get a clone of the underlying terminal for direct access
195    #[allow(dead_code)]
196    pub fn terminal(&self) -> Arc<Mutex<Terminal>> {
197        let pty = self.pty_session.lock();
198        pty.terminal()
199    }
200
201    /// Check if there have been updates since last check
202    #[allow(dead_code)]
203    pub fn has_updates(&self) -> bool {
204        // For now, always assume there are updates since we poll at 60fps
205        // In the future, we could track update generation to optimize
206        true
207    }
208
209    /// Check if the PTY is still running
210    pub fn is_running(&self) -> bool {
211        let pty = self.pty_session.lock();
212        pty.is_running()
213    }
214
215    /// Kill the PTY process
216    pub fn kill(&mut self) -> Result<()> {
217        let mut pty = self.pty_session.lock();
218        pty.kill()
219            .map_err(|e| anyhow::anyhow!("Failed to kill PTY: {:?}", e))
220    }
221
222    /// Get the current bell event count
223    pub fn bell_count(&self) -> u64 {
224        let pty = self.pty_session.lock();
225        pty.bell_count()
226    }
227
228    /// Get scrollback lines
229    #[allow(dead_code)]
230    pub fn scrollback(&self) -> Vec<String> {
231        let pty = self.pty_session.lock();
232        pty.scrollback()
233    }
234
235    /// Get scrollback length
236    pub fn scrollback_len(&self) -> usize {
237        let pty = self.pty_session.lock();
238        pty.scrollback_len()
239    }
240
241    /// Clear scrollback buffer
242    ///
243    /// Removes all scrollback history while preserving the current screen content.
244    /// Uses CSI 3 J (ED 3) escape sequence which is the standard way to clear scrollback.
245    pub fn clear_scrollback(&self) {
246        let pty = self.pty_session.lock();
247        let terminal = pty.terminal();
248        let mut term = terminal.lock();
249        // CSI 3 J = ESC [ 3 J - Erase Scrollback (ED 3)
250        term.process(b"\x1b[3J");
251    }
252
253    /// Take all pending OSC 9/777 notifications
254    pub fn take_notifications(&self) -> Vec<par_term_emu_core_rust::terminal::Notification> {
255        let pty = self.pty_session.lock();
256        let terminal = pty.terminal();
257        let mut term = terminal.lock();
258        term.take_notifications()
259    }
260
261    /// Check if there are pending OSC 9/777 notifications
262    pub fn has_notifications(&self) -> bool {
263        let pty = self.pty_session.lock();
264        let terminal = pty.terminal();
265        let term = terminal.lock();
266        term.has_notifications()
267    }
268
269    /// Take a screenshot of the terminal and save to file
270    ///
271    /// # Arguments
272    /// * `path` - Path to save the screenshot
273    /// * `format` - Screenshot format ("png", "jpeg", "svg", "html")
274    /// * `scrollback_lines` - Number of scrollback lines to include (0 for none)
275    #[allow(dead_code)]
276    pub fn screenshot_to_file(
277        &self,
278        path: &std::path::Path,
279        format: &str,
280        scrollback_lines: usize,
281    ) -> Result<()> {
282        use par_term_emu_core_rust::screenshot::{ImageFormat, ScreenshotConfig};
283
284        log::info!(
285            "Taking screenshot to: {} (format: {}, scrollback: {})",
286            path.display(),
287            format,
288            scrollback_lines
289        );
290
291        let pty = self.pty_session.lock();
292        let terminal = pty.terminal();
293        let term = terminal.lock();
294
295        // Map format string to ImageFormat enum
296        let image_format = match format.to_lowercase().as_str() {
297            "png" => ImageFormat::Png,
298            "jpeg" | "jpg" => ImageFormat::Jpeg,
299            "svg" => ImageFormat::Svg,
300            _ => {
301                log::warn!("Unknown format '{}', defaulting to PNG", format);
302                ImageFormat::Png
303            }
304        };
305
306        // Create screenshot config
307        let config = ScreenshotConfig {
308            format: image_format,
309            ..Default::default()
310        };
311
312        // Call the core library's screenshot method
313        term.screenshot_to_file(path, config, scrollback_lines)
314            .map_err(|e| anyhow::anyhow!("Failed to save screenshot: {}", e))?;
315
316        log::info!("Screenshot saved successfully");
317        Ok(())
318    }
319
320    // TODO: Recording APIs not yet available in par-term-emu-core-rust
321    // Uncomment when the core library supports recording again
322
323    /*
324    /// Start recording a terminal session
325    pub fn start_recording(&self, title: Option<String>) {
326        log::info!("Starting session recording");
327        let pty = self.pty_session.lock();
328        let terminal = pty.terminal();
329        let mut term = terminal.lock();
330        term.start_recording(title);
331    }
332
333    /// Stop recording and return the recording session
334    pub fn stop_recording(&self) -> Option<par_term_emu_core_rust::terminal::RecordingSession> {
335        log::info!("Stopping session recording");
336        let pty = self.pty_session.lock();
337        let terminal = pty.terminal();
338        let mut term = terminal.lock();
339        term.stop_recording()
340    }
341
342    /// Add a marker to the recording
343    pub fn record_marker(&self, label: String) {
344        log::debug!("Recording marker: {}", label);
345        let pty = self.pty_session.lock();
346        let terminal = pty.terminal();
347        let mut term = terminal.lock();
348        term.record_marker(label);
349    }
350
351    /// Export recording to file (asciicast or JSON format)
352    pub fn export_recording_to_file(
353        &self,
354        session: &par_term_emu_core_rust::terminal::RecordingSession,
355        path: &std::path::Path,
356        format: &str,
357    ) -> Result<()> {
358        log::info!("Exporting recording to {}: {}", format, path.display());
359        let pty = self.pty_session.lock();
360        let terminal = pty.terminal();
361        let term = terminal.lock();
362
363        let content = match format.to_lowercase().as_str() {
364            "json" => term.export_json(session),
365            _ => term.export_asciicast(session), // default to asciicast
366        };
367
368        std::fs::write(path, content)?;
369        log::info!("Recording exported successfully");
370        Ok(())
371    }
372
373    /// Check if currently recording
374    pub fn is_recording(&self) -> bool {
375        let pty = self.pty_session.lock();
376        let terminal = pty.terminal();
377        let term = terminal.lock();
378        term.is_recording()
379    }
380    */
381
382    /// Get current working directory from shell integration (OSC 7)
383    pub fn shell_integration_cwd(&self) -> Option<String> {
384        let pty = self.pty_session.lock();
385        let terminal = pty.terminal();
386        let term = terminal.lock();
387        term.shell_integration().cwd().map(String::from)
388    }
389
390    /// Get last command exit code from shell integration (OSC 133)
391    pub fn shell_integration_exit_code(&self) -> Option<i32> {
392        let pty = self.pty_session.lock();
393        let terminal = pty.terminal();
394        let term = terminal.lock();
395        term.shell_integration().exit_code()
396    }
397
398    /// Get current command from shell integration
399    #[allow(dead_code)]
400    pub fn shell_integration_command(&self) -> Option<String> {
401        let pty = self.pty_session.lock();
402        let terminal = pty.terminal();
403        let term = terminal.lock();
404        term.shell_integration().command().map(String::from)
405    }
406
407    // TODO: Shell integration stats API not yet available in par-term-emu-core-rust
408    /*
409    /// Get shell integration statistics
410    pub fn shell_integration_stats(
411        &self,
412    ) -> par_term_emu_core_rust::terminal::ShellIntegrationStats {
413        let pty = self.pty_session.lock();
414        let terminal = pty.terminal();
415        let term = terminal.lock();
416        term.get_shell_integration_stats()
417    }
418    */
419
420    /// Get cursor position
421    #[allow(dead_code)]
422    pub fn cursor_position(&self) -> (usize, usize) {
423        let pty = self.pty_session.lock();
424        pty.cursor_position()
425    }
426
427    /// Get cursor style from terminal for rendering
428    pub fn cursor_style(&self) -> par_term_emu_core_rust::cursor::CursorStyle {
429        let pty = self.pty_session.lock();
430        let terminal = pty.terminal();
431        let term = terminal.lock();
432        term.cursor().style()
433    }
434
435    /// Set cursor style for the terminal
436    pub fn set_cursor_style(&mut self, style: par_term_emu_core_rust::cursor::CursorStyle) {
437        let pty = self.pty_session.lock();
438        let terminal = pty.terminal();
439        let mut term = terminal.lock();
440        term.set_cursor_style(style);
441    }
442
443    /// Check if cursor is visible (controlled by DECTCEM escape sequence)
444    ///
445    /// TUI applications typically hide the cursor when entering alternate screen mode.
446    /// Returns false when the terminal has received CSI ?25l (hide cursor).
447    pub fn is_cursor_visible(&self) -> bool {
448        let pty = self.pty_session.lock();
449        let terminal = pty.terminal();
450        let term = terminal.lock();
451        term.cursor().visible
452    }
453
454    /// Check if mouse tracking is enabled
455    pub fn is_mouse_tracking_enabled(&self) -> bool {
456        let pty = self.pty_session.lock();
457        let terminal = pty.terminal();
458        let term = terminal.lock();
459        !matches!(
460            term.mouse_mode(),
461            par_term_emu_core_rust::mouse::MouseMode::Off
462        )
463    }
464
465    /// Check if alternate screen is active (used by TUI applications)
466    ///
467    /// When the alternate screen is active, text selection should typically be disabled
468    /// as the content is controlled by an application (vim, htop, etc.) rather than
469    /// being scrollback history.
470    pub fn is_alt_screen_active(&self) -> bool {
471        let pty = self.pty_session.lock();
472        let terminal = pty.terminal();
473        let term = terminal.lock();
474        term.is_alt_screen_active()
475    }
476
477    /// Get the terminal title set by OSC 0, 1, or 2 sequences
478    ///
479    /// Returns the title string that applications have set via escape sequences.
480    /// Returns empty string if no title has been set.
481    pub fn get_title(&self) -> String {
482        let pty = self.pty_session.lock();
483        let terminal = pty.terminal();
484        let term = terminal.lock();
485        term.title().to_string()
486    }
487
488    /// Check if mouse motion events should be reported
489    /// Returns true if mode is ButtonEvent or AnyEvent
490    pub fn should_report_mouse_motion(&self, button_pressed: bool) -> bool {
491        let pty = self.pty_session.lock();
492        let terminal = pty.terminal();
493        let term = terminal.lock();
494
495        match term.mouse_mode() {
496            par_term_emu_core_rust::mouse::MouseMode::AnyEvent => true,
497            par_term_emu_core_rust::mouse::MouseMode::ButtonEvent => button_pressed,
498            _ => false,
499        }
500    }
501
502    /// Send a mouse event to the terminal and get the encoded bytes
503    ///
504    /// # Arguments
505    /// * `button` - Mouse button (0 = left, 1 = middle, 2 = right)
506    /// * `col` - Column position (0-indexed)
507    /// * `row` - Row position (0-indexed)
508    /// * `pressed` - true for press, false for release
509    /// * `modifiers` - Modifier keys bit mask
510    ///
511    /// # Returns
512    /// Encoded mouse event bytes to send to PTY, or empty vec if tracking is disabled
513    pub fn encode_mouse_event(
514        &self,
515        button: u8,
516        col: usize,
517        row: usize,
518        pressed: bool,
519        modifiers: u8,
520    ) -> Vec<u8> {
521        let pty = self.pty_session.lock();
522        let terminal = pty.terminal();
523        let mut term = terminal.lock();
524
525        let mouse_event =
526            par_term_emu_core_rust::mouse::MouseEvent::new(button, col, row, pressed, modifiers);
527        term.report_mouse(mouse_event)
528    }
529
530    /// Get styled segments from the terminal for rendering
531    #[allow(dead_code)]
532    pub fn get_styled_segments(&self) -> Vec<StyledSegment> {
533        let pty = self.pty_session.lock();
534        let terminal = pty.terminal();
535        let term = terminal.lock();
536        let grid = term.active_grid();
537        extract_styled_segments(grid)
538    }
539
540    /// Get the current generation number for dirty tracking
541    ///
542    /// The generation number increments whenever the terminal content changes.
543    /// This can be used to detect when a cached representation needs to be updated.
544    pub fn update_generation(&self) -> u64 {
545        let pty = self.pty_session.lock();
546        pty.update_generation()
547    }
548}
549
550// ========================================================================
551// Clipboard History Methods
552// ========================================================================
553
554impl TerminalManager {}
555
556impl Drop for TerminalManager {
557    fn drop(&mut self) {
558        log::info!("Shutting down terminal manager");
559
560        // Explicitly clean up PTY session to ensure proper shutdown
561        if let Some(mut pty) = self.pty_session.try_lock() {
562            // Kill any running process
563            if pty.is_running() {
564                log::info!("Killing PTY process during shutdown");
565                if let Err(e) = pty.kill() {
566                    log::warn!("Failed to kill PTY process: {:?}", e);
567                }
568            }
569        } else {
570            log::warn!("Could not acquire PTY lock during terminal manager shutdown");
571        }
572
573        log::info!("Terminal manager shutdown complete");
574    }
575}