tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Confirmation popup widgets.

use ratatui::{
    buffer::Buffer,
    layout::{Alignment, Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Clear, Paragraph, Widget},
};

use super::{PopupSizing, SizeHint};
use crate::tui::components::{ButtonRow, button_presets};
use crate::tui::theme::{self, BlockVariant};

/// Confirm quit popup widget
pub struct ConfirmQuitPopup {
    /// Selected option (false = No, true = Yes)
    pub selected_yes: bool,
}

impl ConfirmQuitPopup {
    /// Create new confirm quit popup
    #[must_use]
    pub fn new() -> Self {
        Self {
            selected_yes: false,
        }
    }

    /// Set selected to Yes
    #[must_use]
    pub fn select_yes(mut self) -> Self {
        self.selected_yes = true;
        self
    }

    /// Set selected to No
    #[must_use]
    pub fn select_no(mut self) -> Self {
        self.selected_yes = false;
        self
    }
}

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

impl PopupSizing for ConfirmQuitPopup {
    fn size_hint(&self) -> SizeHint {
        // Layout: border(2) + padding(1) + message(1) + padding(1) + buttons(1) = 6
        SizeHint::percent(0, 0)
            .with_min_width(40)
            .with_min_height(6)
    }
}

impl Widget for ConfirmQuitPopup {
    fn render(self, area: Rect, buf: &mut Buffer) {
        Clear.render(area, buf);

        let block = theme::block("Quit tazuna?", BlockVariant::Warning);
        let inner = block.inner(area);
        block.render(area, buf);

        // Layout: padding(1) | message(1) | padding(1) | buttons(1)
        let chunks = Layout::vertical([
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Length(1),
        ])
        .split(inner);

        // Message (centered)
        Paragraph::new("All sessions will be terminated.")
            .alignment(Alignment::Center)
            .render(chunks[1], buf);

        // Buttons (centered)
        let selected = usize::from(!self.selected_yes);
        ButtonRow::new(&[button_presets::YES, button_presets::NO])
            .selected(selected)
            .centered()
            .render(chunks[3], buf);
    }
}

/// Confirm dangerous permissions popup widget
///
/// Asks user to confirm using --dangerously-skip-permissions flag.
pub struct ConfirmPermissionsPopup<'a> {
    /// Branch name to be created
    branch: &'a str,
    /// Selected option (false = No, true = Yes)
    pub selected_yes: bool,
}

impl<'a> ConfirmPermissionsPopup<'a> {
    /// Create new confirm permissions popup
    #[must_use]
    pub fn new(branch: &'a str) -> Self {
        Self {
            branch,
            selected_yes: false,
        }
    }

    /// Set selected to Yes
    #[must_use]
    pub fn select_yes(mut self) -> Self {
        self.selected_yes = true;
        self
    }

    /// Set selected to No
    #[must_use]
    pub fn select_no(mut self) -> Self {
        self.selected_yes = false;
        self
    }
}

impl PopupSizing for ConfirmPermissionsPopup<'_> {
    fn size_hint(&self) -> SizeHint {
        // Layout: border(2) + padding(1) + content(5) + padding(1) + buttons(1) = 10
        SizeHint::percent(0, 0)
            .with_min_width(50)
            .with_min_height(10)
    }
}

impl Widget for ConfirmPermissionsPopup<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        Clear.render(area, buf);

        let block = theme::block(" âš  Dangerous Permissions Mode ", BlockVariant::Warning);
        let inner = block.inner(area);
        block.render(area, buf);

        // Layout: padding(1) | content(5) | padding(1) | buttons(1)
        let chunks = Layout::vertical([
            Constraint::Length(1), // padding
            Constraint::Length(5), // content
            Constraint::Length(1), // padding
            Constraint::Length(1), // buttons
        ])
        .split(inner);

        // Content (centered lines)
        let content_text = vec![
            Line::from(Span::styled(
                "This will start Claude Code with:",
                Style::default().fg(Color::White),
            )),
            Line::from(Span::styled(
                "--dangerously-skip-permissions",
                Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
            )),
            Line::from(""),
            Line::from(vec![
                Span::styled("Branch: ", Style::default().fg(Color::Gray)),
                Span::styled(self.branch, Style::default().fg(Color::Yellow)),
            ]),
            Line::from(Span::styled(
                "Claude can run any command without asking.",
                Style::default().fg(Color::Gray),
            )),
        ];
        Paragraph::new(content_text)
            .alignment(Alignment::Center)
            .render(chunks[1], buf);

        // Buttons (centered)
        let selected = usize::from(!self.selected_yes);
        ButtonRow::new(&[button_presets::YES_UNDERSTAND, button_presets::NO_CANCEL])
            .selected(selected)
            .centered()
            .render(chunks[3], buf);
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::tui::test_utils::buffer_to_text;

    // ConfirmQuitPopup tests
    #[test]
    fn test_confirm_quit_popup_default() {
        let popup = ConfirmQuitPopup::new();
        assert!(!popup.selected_yes);
    }

    #[test]
    fn test_confirm_quit_popup_renders() {
        let popup = ConfirmQuitPopup::new();
        let area = Rect::new(0, 0, 40, 6);
        let mut buf = Buffer::empty(area);

        popup.render(area, &mut buf);

        let output = buffer_to_text(&buf);

        assert!(output.contains("Quit"));
        assert!(output.contains("[Y]es"));
        assert!(output.contains("[N]o"));
    }

    #[test]
    fn confirm_quit_popup_select_yes() {
        let popup = ConfirmQuitPopup::new().select_yes();
        assert!(popup.selected_yes);
    }

    #[test]
    fn confirm_quit_popup_select_no() {
        let popup = ConfirmQuitPopup::new().select_yes().select_no();
        assert!(!popup.selected_yes);
    }

    #[test]
    fn confirm_quit_popup_default_impl() {
        let popup = ConfirmQuitPopup::default();
        assert!(!popup.selected_yes);
    }

    #[test]
    fn confirm_quit_popup_renders_yes_selected() {
        let popup = ConfirmQuitPopup::new().select_yes();
        let area = Rect::new(0, 0, 40, 6);
        let mut buf = Buffer::empty(area);

        popup.render(area, &mut buf);

        let output = buffer_to_text(&buf);
        assert!(output.contains("[Y]es"));
        assert!(output.contains("[N]o"));
    }

    // ConfirmPermissionsPopup tests
    #[test]
    fn confirm_permissions_popup_new() {
        let popup = ConfirmPermissionsPopup::new("feat/issue-42");
        assert!(!popup.selected_yes);
    }

    #[test]
    fn confirm_permissions_popup_select_yes() {
        let popup = ConfirmPermissionsPopup::new("feat/issue-42").select_yes();
        assert!(popup.selected_yes);
    }

    #[test]
    fn confirm_permissions_popup_select_no() {
        let popup = ConfirmPermissionsPopup::new("feat/issue-42")
            .select_yes()
            .select_no();
        assert!(!popup.selected_yes);
    }

    #[test]
    fn confirm_permissions_popup_renders() {
        let popup = ConfirmPermissionsPopup::new("feat/issue-42");
        let area = Rect::new(0, 0, 60, 12);
        let mut buf = Buffer::empty(area);

        popup.render(area, &mut buf);

        let output = buffer_to_text(&buf);
        assert!(output.contains("Dangerous Permissions"));
        assert!(output.contains("--dangerously-skip-permissions"));
        assert!(output.contains("feat/issue-42"));
        assert!(output.contains("[Y]es"));
        assert!(output.contains("[N]o"));
    }

    #[test]
    fn confirm_permissions_popup_renders_yes_selected() {
        let popup = ConfirmPermissionsPopup::new("test-branch").select_yes();
        let area = Rect::new(0, 0, 60, 12);
        let mut buf = Buffer::empty(area);

        popup.render(area, &mut buf);

        let output = buffer_to_text(&buf);
        assert!(output.contains("test-branch"));
    }

    mod snapshots {
        use super::*;
        use crate::tui::test_utils::render_to_snapshot;
        use insta::assert_snapshot;

        #[test]
        fn confirm_quit_no_selected() {
            let popup = ConfirmQuitPopup::new();
            assert_snapshot!(render_to_snapshot(popup, 40, 6));
        }

        #[test]
        fn confirm_quit_yes_selected() {
            let popup = ConfirmQuitPopup::new().select_yes();
            assert_snapshot!(render_to_snapshot(popup, 40, 6));
        }
    }
}