use crate::components::{Box as TinkBox, Text};
use crate::core::{Color, Element, FlexDirection};
#[derive(Debug, Clone)]
pub struct ConfirmState {
prompt: String,
focused_yes: bool,
result: Option<bool>,
default: bool,
}
impl ConfirmState {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
focused_yes: false, result: None,
default: false,
}
}
pub fn default_yes(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
focused_yes: true,
result: None,
default: true,
}
}
pub fn default_no(prompt: impl Into<String>) -> Self {
Self::new(prompt)
}
pub fn prompt(&self) -> &str {
&self.prompt
}
pub fn set_prompt(&mut self, prompt: impl Into<String>) {
self.prompt = prompt.into();
}
pub fn is_yes_focused(&self) -> bool {
self.focused_yes
}
pub fn is_no_focused(&self) -> bool {
!self.focused_yes
}
pub fn focus_yes(&mut self) {
self.focused_yes = true;
}
pub fn focus_no(&mut self) {
self.focused_yes = false;
}
pub fn toggle_focus(&mut self) {
self.focused_yes = !self.focused_yes;
}
pub fn confirm(&mut self) {
self.result = Some(true);
}
pub fn cancel(&mut self) {
self.result = Some(false);
}
pub fn submit(&mut self) {
self.result = Some(self.focused_yes);
}
pub fn result(&self) -> Option<bool> {
self.result
}
pub fn is_confirmed(&self) -> bool {
self.result == Some(true)
}
pub fn is_cancelled(&self) -> bool {
self.result == Some(false)
}
pub fn is_answered(&self) -> bool {
self.result.is_some()
}
pub fn reset(&mut self) {
self.result = None;
self.focused_yes = self.default;
}
pub fn default(&self) -> bool {
self.default
}
}
#[derive(Debug, Clone)]
pub struct ConfirmStyle {
pub yes_label: String,
pub no_label: String,
pub separator: String,
pub focused_color: Option<Color>,
pub focused_bg: Option<Color>,
pub unfocused_color: Option<Color>,
pub prompt_color: Option<Color>,
pub show_hints: bool,
pub button_style: ButtonStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonStyle {
#[default]
Brackets,
Angles,
Parens,
Plain,
Padded,
}
impl Default for ConfirmStyle {
fn default() -> Self {
Self {
yes_label: "Yes".to_string(),
no_label: "No".to_string(),
separator: " ".to_string(),
focused_color: Some(Color::White),
focused_bg: Some(Color::Cyan),
unfocused_color: Some(Color::BrightBlack),
prompt_color: None,
show_hints: true,
button_style: ButtonStyle::Brackets,
}
}
}
impl ConfirmStyle {
pub fn new() -> Self {
Self::default()
}
pub fn yes_label(mut self, label: impl Into<String>) -> Self {
self.yes_label = label.into();
self
}
pub fn no_label(mut self, label: impl Into<String>) -> Self {
self.no_label = label.into();
self
}
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
}
pub fn separator(mut self, sep: impl Into<String>) -> Self {
self.separator = sep.into();
self
}
pub fn focused_color(mut self, color: Color) -> Self {
self.focused_color = Some(color);
self
}
pub fn focused_bg(mut self, color: Color) -> Self {
self.focused_bg = Some(color);
self
}
pub fn unfocused_color(mut self, color: Color) -> Self {
self.unfocused_color = Some(color);
self
}
pub fn prompt_color(mut self, color: Color) -> Self {
self.prompt_color = Some(color);
self
}
pub fn show_hints(mut self, show: bool) -> Self {
self.show_hints = show;
self
}
pub fn button_style(mut self, style: ButtonStyle) -> Self {
self.button_style = style;
self
}
fn format_button(&self, label: &str, hint: Option<char>) -> String {
let hint_str = if self.show_hints {
hint.map(|c| format!("({})", c)).unwrap_or_default()
} else {
String::new()
};
match self.button_style {
ButtonStyle::Brackets => format!("[{}]{}", label, hint_str),
ButtonStyle::Angles => format!("<{}>{}", label, hint_str),
ButtonStyle::Parens => format!("({}){}", label, hint_str),
ButtonStyle::Plain => format!("{}{}", label, hint_str),
ButtonStyle::Padded => format!("[ {} ]{}", label, hint_str),
}
}
pub fn confirm_cancel() -> Self {
Self::default().labels("Confirm", "Cancel")
}
pub fn ok_cancel() -> Self {
Self::default().labels("OK", "Cancel")
}
pub fn save_discard() -> Self {
Self::default().labels("Save", "Discard")
}
pub fn delete_keep() -> Self {
Self::default().labels("Delete", "Keep")
}
}
#[derive(Debug, Clone)]
pub struct Confirm<'a> {
state: &'a ConfirmState,
style: ConfirmStyle,
focused: bool,
}
impl<'a> Confirm<'a> {
pub fn new(state: &'a ConfirmState) -> Self {
Self {
state,
style: ConfirmStyle::default(),
focused: true,
}
}
pub fn style(mut self, style: ConfirmStyle) -> Self {
self.style = style;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn yes_label(mut self, label: impl Into<String>) -> Self {
self.style.yes_label = label.into();
self
}
pub fn no_label(mut self, label: impl Into<String>) -> Self {
self.style.no_label = label.into();
self
}
pub fn render(&self) -> String {
let yes_btn = self.style.format_button(&self.style.yes_label, Some('Y'));
let no_btn = self.style.format_button(&self.style.no_label, Some('N'));
format!(
"{} {}{}{}",
self.state.prompt, yes_btn, self.style.separator, no_btn
)
}
pub fn into_element(self) -> Element {
let mut container = TinkBox::new().flex_direction(FlexDirection::Column);
let mut prompt_text = Text::new(&self.state.prompt);
if let Some(color) = self.style.prompt_color {
prompt_text = prompt_text.color(color);
}
container = container.child(prompt_text.into_element());
let mut buttons = TinkBox::new().flex_direction(FlexDirection::Row);
let yes_label = self.style.format_button(&self.style.yes_label, Some('Y'));
let mut yes_text = Text::new(&yes_label);
if self.focused && self.state.is_yes_focused() {
if let Some(color) = self.style.focused_color {
yes_text = yes_text.color(color);
}
if let Some(bg) = self.style.focused_bg {
yes_text = yes_text.background(bg);
}
yes_text = yes_text.bold();
} else if let Some(color) = self.style.unfocused_color {
yes_text = yes_text.color(color);
}
buttons = buttons.child(yes_text.into_element());
buttons = buttons.child(Text::new(&self.style.separator).into_element());
let no_label = self.style.format_button(&self.style.no_label, Some('N'));
let mut no_text = Text::new(&no_label);
if self.focused && self.state.is_no_focused() {
if let Some(color) = self.style.focused_color {
no_text = no_text.color(color);
}
if let Some(bg) = self.style.focused_bg {
no_text = no_text.background(bg);
}
no_text = no_text.bold();
} else if let Some(color) = self.style.unfocused_color {
no_text = no_text.color(color);
}
buttons = buttons.child(no_text.into_element());
container = container.child(buttons.into_element());
container.into_element()
}
}
pub fn handle_confirm_input(
state: &mut ConfirmState,
input: &str,
key: &crate::hooks::Key,
) -> bool {
if state.is_answered() {
return false;
}
let mut handled = false;
if key.tab || key.left_arrow || key.right_arrow {
state.toggle_focus();
handled = true;
}
else if key.return_key || key.space {
state.submit();
handled = true;
}
else if input.eq_ignore_ascii_case("y") {
state.confirm();
handled = true;
}
else if input.eq_ignore_ascii_case("n") || key.escape {
state.cancel();
handled = true;
}
handled
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confirm_state_new() {
let state = ConfirmState::new("Delete?");
assert_eq!(state.prompt(), "Delete?");
assert!(!state.is_yes_focused());
assert!(state.is_no_focused());
assert!(!state.is_answered());
}
#[test]
fn test_confirm_state_default_yes() {
let state = ConfirmState::default_yes("Continue?");
assert!(state.is_yes_focused());
assert!(!state.is_no_focused());
}
#[test]
fn test_confirm_state_toggle() {
let mut state = ConfirmState::new("Test?");
assert!(state.is_no_focused());
state.toggle_focus();
assert!(state.is_yes_focused());
state.toggle_focus();
assert!(state.is_no_focused());
}
#[test]
fn test_confirm_state_confirm() {
let mut state = ConfirmState::new("Test?");
state.confirm();
assert!(state.is_answered());
assert!(state.is_confirmed());
assert!(!state.is_cancelled());
assert_eq!(state.result(), Some(true));
}
#[test]
fn test_confirm_state_cancel() {
let mut state = ConfirmState::new("Test?");
state.cancel();
assert!(state.is_answered());
assert!(!state.is_confirmed());
assert!(state.is_cancelled());
assert_eq!(state.result(), Some(false));
}
#[test]
fn test_confirm_state_submit() {
let mut state = ConfirmState::new("Test?");
state.focus_yes();
state.submit();
assert!(state.is_confirmed());
let mut state = ConfirmState::new("Test?");
state.focus_no();
state.submit();
assert!(state.is_cancelled());
}
#[test]
fn test_confirm_state_reset() {
let mut state = ConfirmState::default_yes("Test?");
state.focus_no();
state.confirm();
state.reset();
assert!(!state.is_answered());
assert!(state.is_yes_focused()); }
#[test]
fn test_confirm_style_presets() {
let _default = ConfirmStyle::default();
let _confirm_cancel = ConfirmStyle::confirm_cancel();
let _ok_cancel = ConfirmStyle::ok_cancel();
let _save_discard = ConfirmStyle::save_discard();
let _delete_keep = ConfirmStyle::delete_keep();
}
#[test]
fn test_confirm_render() {
let state = ConfirmState::new("Delete file?");
let confirm = Confirm::new(&state);
let rendered = confirm.render();
assert!(rendered.contains("Delete file?"));
assert!(rendered.contains("Yes"));
assert!(rendered.contains("No"));
}
}