use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Widget},
};
use crate::ui::ConfirmChoice;
pub struct ConfirmWidget<'a> {
title: &'a str,
message: &'a str,
commands: &'a [String],
selected: ConfirmChoice,
}
impl<'a> ConfirmWidget<'a> {
pub fn new(
title: &'a str,
message: &'a str,
commands: &'a [String],
selected: ConfirmChoice,
) -> Self {
Self {
title,
message,
commands,
selected,
}
}
}
impl Widget for ConfirmWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.width < 40 || area.height < 12 {
return;
}
let mut y = area.y;
let title_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
buf.set_string(area.x, y, self.title, title_style);
y += 2;
buf.set_string(area.x, y, self.message, Style::default().fg(Color::White));
y += 2;
if !self.commands.is_empty() {
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray))
.title("Commands to execute");
let cmd_height = (self.commands.len() as u16 + 2).min(8);
let cmd_width = area.width.min(60);
let cmd_area = Rect::new(area.x, y, cmd_width, cmd_height);
let inner = block.inner(cmd_area);
block.render(cmd_area, buf);
for (i, cmd) in self.commands.iter().enumerate() {
if inner.y + i as u16 >= inner.y + inner.height {
break;
}
buf.set_string(
inner.x,
inner.y + i as u16,
format!("$ {}", cmd),
Style::default().fg(Color::Cyan),
);
}
y += cmd_height + 1;
}
let choices = [
ConfirmChoice::Trust,
ConfirmChoice::Once,
ConfirmChoice::SkipHooks,
];
for choice in &choices {
if y >= area.y + area.height - 2 {
break;
}
let is_selected = *choice == self.selected;
let (prefix, style) = if is_selected {
(
"▶ ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(Color::White))
};
buf.set_string(area.x, y, prefix, style);
let label = format!(
"[{}] {}",
choice.label().chars().next().unwrap_or('?'),
choice.label()
);
buf.set_string(area.x + 2, y, &label, style);
buf.set_string(
area.x + 15,
y,
format!("- {}", choice.description()),
Style::default().fg(Color::DarkGray),
);
y += 1;
}
y += 1;
if y < area.y + area.height {
let help_line = Line::from(vec![
Span::styled("↑/↓", Style::default().fg(Color::Cyan)),
Span::raw(" navigate • "),
Span::styled("Enter", Style::default().fg(Color::Green)),
Span::raw(" confirm • "),
Span::styled("T/O/C", Style::default().fg(Color::Yellow)),
Span::raw(" quick select • "),
Span::styled("Esc", Style::default().fg(Color::Red)),
Span::raw(" cancel"),
]);
buf.set_line(area.x, y, &help_line, area.width);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confirm_widget_creation() {
let commands = vec!["npm install".to_string()];
let widget = ConfirmWidget::new(
"Hook Confirmation",
"The following hooks will be executed:",
&commands,
ConfirmChoice::Once,
);
assert_eq!(widget.title, "Hook Confirmation");
assert_eq!(widget.selected, ConfirmChoice::Once);
}
#[test]
fn test_confirm_widget_empty_commands() {
let commands: Vec<String> = vec![];
let widget = ConfirmWidget::new(
"Confirm",
"No commands to execute",
&commands,
ConfirmChoice::SkipHooks,
);
assert_eq!(widget.commands.len(), 0);
assert_eq!(widget.selected, ConfirmChoice::SkipHooks);
}
#[test]
fn test_confirm_widget_all_choices() {
let commands = vec!["test".to_string()];
let widget_trust = ConfirmWidget::new("Title", "Message", &commands, ConfirmChoice::Trust);
assert_eq!(widget_trust.selected, ConfirmChoice::Trust);
let widget_once = ConfirmWidget::new("Title", "Message", &commands, ConfirmChoice::Once);
assert_eq!(widget_once.selected, ConfirmChoice::Once);
let widget_cancel =
ConfirmWidget::new("Title", "Message", &commands, ConfirmChoice::SkipHooks);
assert_eq!(widget_cancel.selected, ConfirmChoice::SkipHooks);
}
#[test]
fn test_confirm_widget_many_commands() {
let commands: Vec<String> = (0..10).map(|i| format!("command_{}", i)).collect();
let widget = ConfirmWidget::new(
"Multiple Commands",
"Many commands to execute",
&commands,
ConfirmChoice::Once,
);
assert_eq!(widget.commands.len(), 10);
assert_eq!(widget.title, "Multiple Commands");
}
#[test]
fn test_confirm_widget_message_content() {
let commands = vec!["npm install".to_string(), "npm test".to_string()];
let widget = ConfirmWidget::new(
"Run hooks?",
"First time running hooks for this project",
&commands,
ConfirmChoice::Once,
);
assert_eq!(widget.message, "First time running hooks for this project");
assert_eq!(widget.commands.len(), 2);
}
}