use crate::element::{Component, Element};
use crate::style::{Color, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConfirmStyle {
#[default]
Inline,
Button,
}
#[derive(Debug, Clone)]
pub struct ConfirmProps {
pub message: String,
pub selected: bool,
pub default: bool,
pub yes_label: String,
pub no_label: String,
pub selected_color: Option<Color>,
pub unselected_color: Option<Color>,
pub inline: bool,
pub separator: String,
pub style: ConfirmStyle,
}
impl Default for ConfirmProps {
fn default() -> Self {
Self {
message: String::new(),
selected: false,
default: false,
yes_label: "Yes".to_string(),
no_label: "No".to_string(),
selected_color: Some(Color::Cyan),
unselected_color: Some(Color::DarkGray),
inline: true,
separator: " / ".to_string(),
style: ConfirmStyle::Inline,
}
}
}
impl ConfirmProps {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
..Default::default()
}
}
#[must_use]
pub fn selected(mut self, yes: bool) -> Self {
self.selected = yes;
self
}
#[must_use]
pub fn default_value(mut self, yes: bool) -> Self {
self.default = yes;
self.selected = yes;
self
}
#[must_use]
pub fn labels(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
self.yes_label = yes.into();
self.no_label = no.into();
self
}
#[must_use]
pub fn selected_color(mut self, color: Color) -> Self {
self.selected_color = Some(color);
self
}
#[must_use]
pub fn unselected_color(mut self, color: Color) -> Self {
self.unselected_color = Some(color);
self
}
#[must_use]
pub fn multiline(mut self) -> Self {
self.inline = false;
self
}
#[must_use]
pub fn separator(mut self, sep: impl Into<String>) -> Self {
self.separator = sep.into();
self
}
#[must_use]
pub fn button_style(mut self) -> Self {
self.style = ConfirmStyle::Button;
self
}
pub fn toggle(&mut self) {
self.selected = !self.selected;
}
pub fn select_yes(&mut self) {
self.selected = true;
}
pub fn select_no(&mut self) {
self.selected = false;
}
pub fn answer(&self) -> bool {
self.selected
}
pub fn render_string(&self) -> String {
match self.style {
ConfirmStyle::Inline => self.render_inline(),
ConfirmStyle::Button => self.render_button(),
}
}
fn render_inline(&self) -> String {
let yes_display = if self.selected {
format!("[{}]", self.yes_label)
} else {
self.yes_label.clone()
};
let no_display = if !self.selected {
format!("[{}]", self.no_label)
} else {
self.no_label.clone()
};
if self.inline {
format!(
"{} {}{}{}",
self.message, yes_display, self.separator, no_display
)
} else {
format!(
"{}\n {}{}{}",
self.message, yes_display, self.separator, no_display
)
}
}
fn render_button(&self) -> String {
let yes_btn = if self.selected {
format!("▸ {} ◂", self.yes_label)
} else {
format!(" {} ", self.yes_label)
};
let no_btn = if !self.selected {
format!("▸ {} ◂", self.no_label)
} else {
format!(" {} ", self.no_label)
};
if self.message.is_empty() {
format!("[{}] [{}]", yes_btn, no_btn)
} else if self.inline {
format!("{}\n[{}] [{}]", self.message, yes_btn, no_btn)
} else {
format!("{}\n\n[{}] [{}]", self.message, yes_btn, no_btn)
}
}
}
pub struct Confirm;
impl Component for Confirm {
type Props = ConfirmProps;
fn render(props: &Self::Props) -> Element {
let content = props.render_string();
let mut style = Style::new();
if let Some(color) = props.selected_color {
style = style.fg(color);
}
Element::styled_text(&content, style)
}
}
pub fn confirm_prompt(message: &str, selected_yes: bool) -> String {
ConfirmProps::new(message)
.selected(selected_yes)
.render_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confirm_props_default() {
let props = ConfirmProps::default();
assert!(props.message.is_empty());
assert!(!props.selected);
assert!(!props.default);
}
#[test]
fn test_confirm_props_new() {
let props = ConfirmProps::new("Delete?");
assert_eq!(props.message, "Delete?");
}
#[test]
fn test_confirm_props_default_value() {
let props = ConfirmProps::new("Continue?").default_value(true);
assert!(props.selected);
assert!(props.default);
}
#[test]
fn test_confirm_props_labels() {
let props = ConfirmProps::new("Save?").labels("Save", "Discard");
assert_eq!(props.yes_label, "Save");
assert_eq!(props.no_label, "Discard");
}
#[test]
fn test_confirm_props_toggle() {
let mut props = ConfirmProps::new("Test").selected(false);
assert!(!props.selected);
props.toggle();
assert!(props.selected);
props.toggle();
assert!(!props.selected);
}
#[test]
fn test_confirm_props_select() {
let mut props = ConfirmProps::new("Test");
props.select_yes();
assert!(props.answer());
props.select_no();
assert!(!props.answer());
}
#[test]
fn test_confirm_render_string_yes_selected() {
let props = ConfirmProps::new("Continue?").selected(true);
let output = props.render_string();
assert!(output.contains("[Yes]"));
assert!(output.contains("No"));
assert!(!output.contains("[No]"));
}
#[test]
fn test_confirm_render_string_no_selected() {
let props = ConfirmProps::new("Continue?").selected(false);
let output = props.render_string();
assert!(output.contains("Yes"));
assert!(output.contains("[No]"));
assert!(!output.contains("[Yes]"));
}
#[test]
fn test_confirm_render_string_custom_labels() {
let props = ConfirmProps::new("Save changes?")
.labels("Save", "Discard")
.selected(true);
let output = props.render_string();
assert!(output.contains("[Save]"));
assert!(output.contains("Discard"));
}
#[test]
fn test_confirm_render_string_multiline() {
let props = ConfirmProps::new("Question?").multiline();
let output = props.render_string();
assert!(output.contains('\n'));
}
#[test]
fn test_confirm_prompt_helper() {
let output = confirm_prompt("Delete?", true);
assert!(output.contains("Delete?"));
assert!(output.contains("[Yes]"));
}
#[test]
fn test_confirm_component_render() {
let props = ConfirmProps::new("Test?").selected(true);
let elem = Confirm::render(&props);
match elem {
Element::Text { content, .. } => {
assert!(content.contains("Test?"));
assert!(content.contains("[Yes]"));
}
_ => panic!("Expected Text element"),
}
}
}