par-term 0.49.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Tab session logging methods.
//!
//! Provides methods for toggling session logging on/off and querying its state.

use crate::config::{Config, SessionLogFormat};
use crate::session_logger::SessionLogger;
use crate::tab::Tab;
use std::sync::Arc;

impl Tab {
    /// Toggle session logging on/off.
    ///
    /// Returns `Ok(true)` if logging is now active, `Ok(false)` if stopped.
    /// If logging wasn't active and no logger exists, creates a new one.
    pub fn toggle_session_logging(&mut self, config: &Config) -> anyhow::Result<bool> {
        let mut logger_guard = self.session_logger.lock();

        if let Some(ref mut logger) = *logger_guard {
            // Logger exists - toggle based on current state
            if logger.is_active() {
                logger.stop()?;
                log::info!("Session logging stopped via hotkey");
                Ok(false)
            } else {
                logger.start()?;
                log::info!("Session logging started via hotkey");
                Ok(true)
            }
        } else {
            // No logger exists - create one and start it
            let logs_dir = config.logs_dir();
            if let Err(e) = std::fs::create_dir_all(&logs_dir) {
                log::warn!("Failed to create logs directory: {}", e);
                return Err(anyhow::anyhow!("Failed to create logs directory: {}", e));
            }
            // SEC-010: Enforce restrictive directory permissions so that session logs
            // (which may capture terminal output including passwords) are not
            // world-listable even if the user's umask is permissive.
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(&logs_dir, std::fs::Permissions::from_mode(0o700));
            }

            // Get terminal dimensions
            let dimensions = if let Ok(term) = self.terminal.try_read() {
                term.dimensions()
            } else {
                (80, 24) // fallback
            };

            let session_title = Some(format!(
                "{} - {}",
                self.title,
                chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
            ));

            let mut logger = SessionLogger::new(
                config.session_log.session_log_format,
                &logs_dir,
                dimensions,
                session_title,
            )?;

            logger.set_redact_passwords(config.session_log.session_log_redact_passwords);

            // v3 logs get the graphics-aware exporter: it ferries the recording
            // to the core lib's export through the terminal at stop() time.
            if config.session_log.session_log_format == SessionLogFormat::AsciicastV3 {
                let tm = Arc::clone(&self.terminal);
                logger.set_v3_exporter(Box::new(move |session| {
                    tm.try_read()
                        .ok()
                        .map(|tm| tm.export_session_asciicast_v3(session))
                }));
            }

            logger.start()?;

            // SEC-002: Emit a prominent one-time warning when session logging is enabled.
            // Session logs capture raw terminal I/O and may include passwords, API keys,
            // tokens, and other credentials even with heuristic redaction active.
            eprintln!(
                "\n[par-term SESSION LOGGING ENABLED]\n\
                 Session output is now being recorded to: {}\n\
                 WARNING: Session logs may capture sensitive data (passwords, API keys, tokens)\n\
                 despite heuristic redaction. Password-prompt detection is enabled: {}\n\
                 Do NOT share session log files unless you have reviewed their contents.\n\
                 Disable session logging when working with sensitive credentials.\n",
                logs_dir.display(),
                config.session_log.session_log_redact_passwords,
            );

            // Set up output callback to record PTY output
            let logger_clone = Arc::clone(&self.session_logger);
            if let Ok(term) = self.terminal.try_read() {
                term.set_output_callback(move |data: &[u8]| {
                    if let Some(ref mut logger) = *logger_clone.lock() {
                        logger.record_output(data);
                    }
                });
            }

            *logger_guard = Some(logger);
            drop(logger_guard);

            // In a mux tab the panes are mirrors fed through
            // `process_mux_output`, which fires the per-terminal output
            // callback — they need the same attachment or their output
            // never reaches the log.
            self.attach_session_logging_to_panes();
            log::info!("Session logging created and started via hotkey");
            Ok(true)
        }
    }

    /// Check if session logging is currently active.
    pub fn is_session_logging_active(&self) -> bool {
        if let Some(ref logger) = *self.session_logger.lock() {
            logger.is_active()
        } else {
            false
        }
    }

    /// Attach the tab's session-logger output callback to every pane
    /// terminal of a tmux/mux tab.
    ///
    /// Mirror panes receive daemon output through `process_mux_output`,
    /// which fires the per-terminal output callback; the toggle/auto-log
    /// paths attach only on `tab.terminal`, which in a mux tab is the
    /// hidden shell whose output is not what the user sees. Called when
    /// logging starts and whenever a tmux layout change creates panes, so
    /// mirrors born mid-session log too. The callback captures the shared
    /// logger slot, so one attached before a later toggle still picks up
    /// the logger stored into it.
    pub(crate) fn attach_session_logging_to_panes(&self) {
        if self.session_logger.lock().is_none() {
            return;
        }
        if !(self.tmux.tmux_gateway_active || self.tmux.tmux_pane_id.is_some()) {
            return;
        }
        if let Some(pm) = self.pane_manager() {
            for pane in pm.all_panes() {
                let logger_clone = Arc::clone(&self.session_logger);
                if let Ok(term) = pane.terminal.try_read() {
                    term.set_output_callback(move |data: &[u8]| {
                        if let Some(ref mut logger) = *logger_clone.lock() {
                            logger.record_output(data);
                        }
                    });
                }
            }
        }
    }
}