par-term 0.48.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Shell exit action handling for WindowState (called from the RedrawRequested event arm).
//!
//! Contains:
//! - `handle_shell_exit`: dispatches shell exit actions (Keep, Close, Restart variants)
//!   for all tabs and panes. Returns true if the window should close.

use crate::app::window_state::WindowState;

impl WindowState {
    /// Handle shell exit based on the configured `shell_exit_action`.
    ///
    /// Returns `true` if the window should close (last tab exited and action is Close).
    pub(crate) fn handle_shell_exit(&mut self) -> bool {
        use crate::config::ShellExitAction;
        use crate::pane::RestartState;

        self.capture_crash_offers();

        match self.config.load().shell.shell_exit_action {
            ShellExitAction::Keep => {
                // Do nothing - keep dead shells showing
            }

            ShellExitAction::Close => {
                // Detect exited panes across all tabs.
                // After R-32, pane_manager is always Some, so we always use the pane path.
                // The primary pane (single-pane tabs) wraps tab.terminal; when it exits
                // close_exited_panes() returns tab_should_close=true just as before.
                let mut tabs_needing_resize: Vec<crate::tab::TabId> = Vec::new();

                // Collect (tab_id, tab_title, exit_notified) for single-pane tabs that exit,
                // so we can fire the session-ended notification before closing.
                let mut single_pane_exiting: Vec<(crate::tab::TabId, String, bool)> = Vec::new();

                let tabs_to_close: Vec<crate::tab::TabId> = self
                    .tab_manager
                    .tabs_mut()
                    .iter_mut()
                    .filter_map(|tab| {
                        if tab.tmux.tmux_gateway_active || tab.tmux.tmux_pane_id.is_some() {
                            return None;
                        }
                        // pane_manager is always Some after R-32.
                        let (closed_panes, tab_should_close) = tab.close_exited_panes();
                        if !closed_panes.is_empty() {
                            log::info!(
                                "Tab {}: closed {} exited pane(s)",
                                tab.id,
                                closed_panes.len()
                            );
                            if !tab_should_close {
                                tabs_needing_resize.push(tab.id);
                            }
                        }
                        if tab_should_close {
                            // Record metadata for notification (single-pane exit path).
                            // Previously this was handled by the legacy pane_manager.is_none()
                            // check; now we capture it here while the tab is still alive.
                            if !tab.has_multiple_panes() {
                                single_pane_exiting.push((
                                    tab.id,
                                    tab.title.clone(),
                                    tab.activity.exit_notified,
                                ));
                                tab.activity.exit_notified = true;
                            }
                            return Some(tab.id);
                        }
                        None
                    })
                    .collect();

                // Send session-ended notifications for single-pane tabs before closing them.
                if self.config.load().notifications.notification_session_ended {
                    for (_tab_id, tab_title, exit_notified) in &single_pane_exiting {
                        if !exit_notified {
                            log::info!("Shell in tab '{}' has exited", tab_title);
                            let title = format!("Shell Exited: {}", tab_title);
                            let message = "The shell process has exited".to_string();
                            self.deliver_notification(&title, &message);
                        }
                    }
                }

                if !tabs_needing_resize.is_empty()
                    && let Some(renderer) = &self.renderer
                {
                    let cell_width = renderer.cell_width();
                    let cell_height = renderer.cell_height();
                    let title_offset = if self.config.load().panes.show_pane_titles {
                        self.config.load().panes.pane_title_height
                    } else {
                        0.0
                    };
                    for tab_id in tabs_needing_resize {
                        if let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
                            && let Some(pm) = tab.pane_manager_mut()
                        {
                            // Suppress padding for single-pane tabs (no dividers).
                            // In split mode, add half the divider width as base so content
                            // doesn't render under the divider, plus the user-configured extra.
                            let padding = if pm.pane_count() <= 1 {
                                0.0
                            } else {
                                self.config.load().panes.pane_divider_width.unwrap_or(2.0) / 2.0
                                    + self.config.load().panes.pane_padding
                            };
                            pm.resize_all_terminals_with_padding(
                                cell_width,
                                cell_height,
                                padding,
                                title_offset,
                            );
                        }
                    }
                }

                for tab_id in &tabs_to_close {
                    log::info!("Closing tab {} - all panes exited", tab_id);
                    if self.tab_manager.visible_tab_count() <= 1 {
                        log::info!("Last tab, closing window");
                        self.is_shutting_down = true;
                        for tab in self.tab_manager.tabs_mut() {
                            tab.stop_refresh_task();
                        }
                        return true;
                    } else {
                        let _ = self.tab_manager.close_tab(*tab_id);
                    }
                }
            }

            ShellExitAction::RestartImmediately
            | ShellExitAction::RestartWithPrompt
            | ShellExitAction::RestartAfterDelay => {
                // Handle restart variants
                let config_clone = (**self.config.load()).clone();

                for tab in self.tab_manager.tabs_mut() {
                    if tab.tmux.tmux_gateway_active || tab.tmux.tmux_pane_id.is_some() {
                        continue;
                    }

                    if let Some(pm) = tab.pane_manager_mut() {
                        for pane in pm.all_panes_mut() {
                            let is_running = pane.is_running();

                            // Check if pane needs restart action
                            if !is_running && pane.restart_state.is_none() {
                                // Shell just exited, handle based on action
                                match self.config.load().shell.shell_exit_action {
                                    ShellExitAction::RestartImmediately => {
                                        log::info!(
                                            "Pane {} shell exited, restarting immediately",
                                            pane.id
                                        );
                                        if let Err(e) = pane.respawn_shell(&config_clone) {
                                            log::error!(
                                                "Failed to respawn shell in pane {}: {}",
                                                pane.id,
                                                e
                                            );
                                        }
                                    }
                                    ShellExitAction::RestartWithPrompt => {
                                        log::info!(
                                            "Pane {} shell exited, showing restart prompt",
                                            pane.id
                                        );
                                        pane.write_restart_prompt();
                                        pane.restart_state = Some(RestartState::AwaitingInput);
                                    }
                                    ShellExitAction::RestartAfterDelay => {
                                        log::info!(
                                            "Pane {} shell exited, will restart after 1s",
                                            pane.id
                                        );
                                        pane.restart_state = Some(RestartState::AwaitingDelay(
                                            std::time::Instant::now(),
                                        ));
                                    }
                                    _ => {}
                                }
                            }

                            // Check if waiting for delay and time has elapsed
                            if let Some(RestartState::AwaitingDelay(exit_time)) =
                                &pane.restart_state
                                && exit_time.elapsed() >= std::time::Duration::from_secs(1)
                            {
                                log::info!("Pane {} delay elapsed, restarting shell", pane.id);
                                if let Err(e) = pane.respawn_shell(&config_clone) {
                                    log::error!(
                                        "Failed to respawn shell in pane {}: {}",
                                        pane.id,
                                        e
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }

        false // Window stays open
    }

    /// One crash-triage capture pass: every dead pane of a non-mux tab not
    /// yet processed is recorded — clean exits are deduped inside `record`.
    /// Runs before the shell-exit actions so the pane's terminal text still
    /// exists when the payload is written (the Close action destroys the
    /// pane in the same frame).
    fn capture_crash_offers(&mut self) {
        let live_tabs: std::collections::HashSet<u64> =
            self.tab_manager.tabs().iter().map(|tab| tab.id).collect();
        self.crash_triage.forget_tabs_except(&live_tabs);

        let config = self.config.load();
        let shell_line = match &config.shell.custom_shell {
            Some(cmd) => {
                let args = config
                    .shell
                    .shell_args
                    .as_ref()
                    .map(|args| args.join(" "))
                    .unwrap_or_default();
                format!("{cmd} {args}")
            }
            None => "user default shell".to_string(),
        };
        drop(config);

        let mut toasts: Vec<String> = Vec::new();
        let mut respawned: Vec<(u64, u64)> = Vec::new();
        for tab in self.tab_manager.tabs() {
            if tab.tmux.tmux_gateway_active || tab.tmux.tmux_pane_id.is_some() {
                continue;
            }
            let Some(pm) = tab.pane_manager() else {
                continue;
            };
            for pane in pm.all_panes() {
                if pane.is_running() {
                    // A running pane with a captured key was respawned — its
                    // next death is a new crash episode.
                    respawned.push((tab.id, pane.id));
                    continue;
                }
                let key = (tab.id, pane.id);
                if self.crash_triage.already_captured(key) {
                    continue;
                }
                // A crash capture must not be dropped on lock contention: the
                // pane is about to be destroyed by the Close action below.
                let terminal = pane.terminal.blocking_read();
                let Some(exit_code) = terminal.exit_code_if_dead() else {
                    // Dead by the reader flag but not yet reaped; a later
                    // pass picks it up.
                    continue;
                };
                let tail = terminal.export_text();
                drop(terminal);
                let label = format!("'{}' pane {}", tab.title, pane.id);
                if self
                    .crash_triage
                    .record(key, label.clone(), exit_code, &shell_line, &tail)
                    .is_some()
                {
                    toasts.push(format!(
                        "Crash captured ({label}, exit {exit_code}) — Triage in the command palette"
                    ));
                }
            }
        }
        for toast in toasts {
            self.show_toast(toast);
        }
        self.crash_triage.clear_captured(&respawned);
    }
}