tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Action selection popup widget.

use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Style},
    text::{Line, Span},
    widgets::{Clear, List, ListItem, StatefulWidget, Widget},
};

use super::state::ActionSelectPopupState;
use super::{PopupSizing, SizeHint};
use crate::github::{ActionChoice, ActionType};
use crate::tui::theme::{self, BlockVariant};

/// Action selection popup widget
///
/// Displays generated action choices for an issue.
pub struct ActionSelectPopup<'a> {
    /// Issue number for title
    issue_number: u32,
    /// Available action choices
    choices: &'a [ActionChoice],
}

impl<'a> ActionSelectPopup<'a> {
    /// Create new action select popup
    #[must_use]
    pub fn new(issue_number: u32, choices: &'a [ActionChoice]) -> Self {
        Self {
            issue_number,
            choices,
        }
    }

    /// Get action type color
    fn action_color(action: ActionType) -> Color {
        match action {
            ActionType::Implement => Color::Green,
            ActionType::Survey => Color::Cyan,
            ActionType::Bugfix => Color::Red,
            ActionType::Refactor => Color::Yellow,
            ActionType::Docs => Color::Magenta,
        }
    }

    /// Truncate string to max length with ellipsis
    fn truncate(s: &str, max_len: usize) -> String {
        if s.len() <= max_len {
            s.to_string()
        } else {
            format!("{}...", &s[..max_len.saturating_sub(3)])
        }
    }
}

impl PopupSizing for ActionSelectPopup<'_> {
    #[allow(clippy::cast_possible_truncation)] // Safe: clamped to max 20
    fn size_hint(&self) -> SizeHint {
        // Dynamic: min_height based on choices count
        // border(2) + choices + padding(1)
        let min_height = (self.choices.len() + 4).min(20) as u16;
        SizeHint::percent(80, 80)
            .with_min_width(60)
            .with_min_height(min_height)
    }
}

impl StatefulWidget for ActionSelectPopup<'_> {
    type State = ActionSelectPopupState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        // Clear background
        Clear.render(area, buf);

        // Calculate available width for prompt (total - action(10) - branch(25) - separators(6))
        let prompt_width = area.width.saturating_sub(45) as usize;

        // Create list items: [icon] action | branch | prompt
        let items: Vec<ListItem> = self
            .choices
            .iter()
            .map(|choice| {
                let icon = Span::styled(
                    format!("[{}] ", choice.action.icon()),
                    Style::default().fg(Self::action_color(choice.action)),
                );
                let action = Span::styled(
                    format!("{:10}", choice.action.as_str()),
                    Style::default().fg(Self::action_color(choice.action)),
                );
                let branch = Span::styled(
                    format!("{:25}", Self::truncate(&choice.branch, 23)),
                    Style::default().fg(Color::Yellow),
                );
                let prompt = Span::styled(
                    Self::truncate(&choice.prompt, prompt_width),
                    Style::default().fg(Color::Gray),
                );
                ListItem::new(Line::from(vec![
                    icon,
                    action,
                    Span::raw(" "),
                    branch,
                    Span::raw(" "),
                    prompt,
                ]))
            })
            .collect();

        let title = format!(
            "Select Action for Issue #{} (Enter: select, Esc/Ctrl-g: cancel)",
            self.issue_number
        );

        let list = List::new(items)
            .block(theme::block(&title, BlockVariant::Focused))
            .highlight_style(theme::highlight_style(true))
            .highlight_symbol(theme::HIGHLIGHT_SYMBOL);

        StatefulWidget::render(list, area, buf, &mut state.list_state);
    }
}

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

    #[test]
    fn action_select_popup_renders() {
        let choices = vec![
            ActionChoice {
                branch: "feat/issue-42".to_string(),
                action: ActionType::Implement,
                prompt: "Implement feature #42".to_string(),
            },
            ActionChoice {
                branch: "survey/issue-42".to_string(),
                action: ActionType::Survey,
                prompt: "Survey codebase for #42".to_string(),
            },
        ];
        let popup = ActionSelectPopup::new(42, &choices);
        let area = Rect::new(0, 0, 80, 10);
        let mut buf = Buffer::empty(area);
        let mut state = ActionSelectPopupState::new();

        popup.render(area, &mut buf, &mut state);

        let output = buffer_to_text(&buf);
        assert!(output.contains("Issue #42"));
        assert!(output.contains("[+]")); // implement icon
        assert!(output.contains("[?]")); // survey icon
        assert!(output.contains("feat/issue-42"));
        assert!(output.contains("Implement"));
    }

    #[test]
    fn action_select_popup_truncate() {
        let short = ActionSelectPopup::truncate("short", 10);
        assert_eq!(short, "short");

        let long = ActionSelectPopup::truncate("this is a very long string", 15);
        assert_eq!(long, "this is a ve...");
    }

    #[test]
    fn action_select_popup_action_color() {
        assert_eq!(
            ActionSelectPopup::action_color(ActionType::Implement),
            Color::Green
        );
        assert_eq!(
            ActionSelectPopup::action_color(ActionType::Survey),
            Color::Cyan
        );
        assert_eq!(
            ActionSelectPopup::action_color(ActionType::Bugfix),
            Color::Red
        );
        assert_eq!(
            ActionSelectPopup::action_color(ActionType::Refactor),
            Color::Yellow
        );
        assert_eq!(
            ActionSelectPopup::action_color(ActionType::Docs),
            Color::Magenta
        );
    }
}