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};
pub struct ActionSelectPopup<'a> {
issue_number: u32,
choices: &'a [ActionChoice],
}
impl<'a> ActionSelectPopup<'a> {
#[must_use]
pub fn new(issue_number: u32, choices: &'a [ActionChoice]) -> Self {
Self {
issue_number,
choices,
}
}
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,
}
}
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)] fn size_hint(&self) -> SizeHint {
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.render(area, buf);
let prompt_width = area.width.saturating_sub(45) as usize;
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("[+]")); assert!(output.contains("[?]")); 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
);
}
}