par-term 0.49.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Quit confirmation dialog for the application.
//!
//! Shows a confirmation dialog when the user attempts to close the window
//! while there are active terminal sessions. Allows the user to either
//! quit the application or cancel the close operation.

/// Action returned by the quit confirmation dialog
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuitConfirmAction {
    /// User confirmed - quit the application
    Quit,
    /// User cancelled - keep the window open
    Cancel,
    /// No action yet (dialog not showing or still showing)
    None,
}

/// State for the quit confirmation dialog
pub struct QuitConfirmationUI {
    /// Whether the dialog is visible
    visible: bool,
    /// Number of active sessions to display
    session_count: usize,
    /// The attached par-mux session, if any — its survival changes what
    /// the dialog may truthfully claim (UX.md M9)
    mux_session: Option<String>,
    /// Shared ConfirmDialog state (UX.md OV3)
    confirm: crate::app::overlay::confirm::ConfirmState,
}

impl Default for QuitConfirmationUI {
    fn default() -> Self {
        Self::new()
    }
}

impl QuitConfirmationUI {
    /// Create a new quit confirmation UI
    pub fn new() -> Self {
        Self {
            visible: false,
            session_count: 0,
            mux_session: None,
            confirm: Default::default(),
        }
    }

    /// Check if the dialog is currently visible
    pub fn is_visible(&self) -> bool {
        self.visible
    }

    /// Show the confirmation dialog with the number of active sessions
    /// and, when a par-mux session is attached, its name (UX.md M9: the
    /// dialog must state that the daemon session survives the quit).
    pub fn show_confirmation(&mut self, session_count: usize, mux_session: Option<&str>) {
        self.visible = true;
        self.session_count = session_count;
        self.mux_session = mux_session.map(str::to_string);
    }

    /// Hide the dialog and clear state
    pub(crate) fn hide(&mut self) {
        self.visible = false;
        self.session_count = 0;
        self.mux_session = None;
        self.confirm.reset();
    }

    /// Render the dialog and return any action.
    ///
    /// Drawn by the shared `ConfirmDialog` (UX.md OV3): Enter and Escape
    /// cancel (MD5 — B64 mapped Enter to Quit), and Quit needs its own
    /// chord or its button.
    pub fn show(&mut self, ctx: &egui::Context) -> QuitConfirmAction {
        use crate::app::overlay::confirm::{self, ConfirmChoice, ConfirmSpec};

        if !self.visible {
            return QuitConfirmAction::None;
        }

        let (session_text, closing_text) =
            summary_lines(self.session_count, self.mux_session.as_deref());
        let body = [session_text, closing_text];
        let spec = ConfirmSpec {
            title: "Quit par-term?",
            body: &body,
            detail: None,
            safe_label: "Cancel",
            alternate_label: None,
            destructive_label: "Quit",
            offers_dont_ask_again: false,
        };
        let Some(answer) = confirm::show(ctx, &spec, &mut self.confirm) else {
            return QuitConfirmAction::None;
        };
        self.hide();
        match answer.choice {
            ConfirmChoice::Destructive => QuitConfirmAction::Quit,
            ConfirmChoice::Safe | ConfirmChoice::Alternate => QuitConfirmAction::Cancel,
        }
    }
}

impl crate::traits::OverlayComponent for QuitConfirmationUI {
    type Action = QuitConfirmAction;

    fn show(&mut self, ctx: &egui::Context) -> Self::Action {
        QuitConfirmationUI::show(self, ctx)
    }

    fn is_visible(&self) -> bool {
        self.is_visible()
    }

    fn set_visible(&mut self, visible: bool) {
        if !visible {
            self.hide();
        }
        // Note: setting visible=true requires session context.
        // Use show_confirmation(session_count, mux_session) to open this dialog.
    }
}

/// UX.md M9: the dialog's two body lines, matched to what quitting
/// actually does. A local-only quit terminates everything, but an
/// attached par-mux session survives the quit (the detach drops the
/// client, the daemon keeps running) — "All sessions will be
/// terminated" was false whenever one was attached.
fn summary_lines(tab_count: usize, mux_session: Option<&str>) -> (String, String) {
    // UX.md T4: "session" means only a par-mux/tmux session, so the count
    // is of tabs, and local shells are said to close.
    let first = if tab_count == 1 {
        "There is 1 open tab.".to_string()
    } else {
        format!("There are {tab_count} open tabs.")
    };
    let second = match mux_session {
        Some(name) => format!("par-mux session '{name}' will keep running (it detaches)."),
        None => "Their shells and running programs will be closed.".to_string(),
    };
    (first, second)
}

#[cfg(test)]
mod tests {
    use super::{QuitConfirmAction, QuitConfirmationUI, summary_lines};

    /// Render one frame with an Enter key press under a headless egui
    /// context and return the action the dialog reported.
    fn show_with_enter(ui: &mut QuitConfirmationUI) -> QuitConfirmAction {
        let ctx = egui::Context::default();
        ctx.begin_pass(egui::RawInput {
            screen_rect: Some(egui::Rect::from_min_size(
                egui::Pos2::ZERO,
                egui::vec2(1200.0, 800.0),
            )),
            events: vec![egui::Event::Key {
                key: egui::Key::Enter,
                physical_key: None,
                pressed: true,
                repeat: false,
                modifiers: Default::default(),
            }],
            ..Default::default()
        });
        let action = ui.show(&ctx);
        ctx.end_pass().textures_delta.clear();
        action
    }

    /// MD5: Enter is the safe choice everywhere — in the quit dialog it
    /// must cancel, never quit. B64 mapped Enter to Quit.
    #[test]
    fn enter_cancels_never_quits() {
        let mut ui = QuitConfirmationUI::new();
        ui.show_confirmation(2, None);
        assert_eq!(show_with_enter(&mut ui), QuitConfirmAction::Cancel);
        assert!(!ui.is_visible());
    }

    /// UX.md M9: with a par-mux session attached, quitting detaches —
    /// the dialog must say the session survives, never "terminated".
    #[test]
    fn mux_attach_summary_claims_survival_not_termination() {
        let (count_line, closing_line) = summary_lines(3, Some("work"));
        assert_eq!(count_line, "There are 3 open tabs.");
        assert_eq!(
            closing_line,
            "par-mux session 'work' will keep running (it detaches)."
        );
    }

    /// The local-only quit says its tabs' shells close, in T4 vocabulary:
    /// it counts tabs and never calls them "sessions".
    #[test]
    fn local_only_summary_still_claims_termination() {
        let (count_line, closing_line) = summary_lines(1, None);
        assert_eq!(count_line, "There is 1 open tab.");
        assert_eq!(
            closing_line,
            "Their shells and running programs will be closed."
        );
        let (count_line, _) = summary_lines(4, None);
        assert_eq!(count_line, "There are 4 open tabs.");
    }
}