use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Constraint, Flex, Layout, Rect},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
use crate::theme::Theme;
#[derive(Debug)]
pub struct ConfirmDialog {
pub visible: bool,
pub title: String,
pub message: String,
pub selected_yes: bool,
}
impl Default for ConfirmDialog {
fn default() -> Self {
Self {
visible: false,
title: String::new(),
message: String::new(),
selected_yes: false,
}
}
}
impl ConfirmDialog {
pub fn new() -> Self {
Self::default()
}
pub fn show(&mut self, title: impl Into<String>, message: impl Into<String>) {
self.title = title.into();
self.message = message.into();
self.selected_yes = false;
self.visible = true;
}
pub fn hide(&mut self) {
self.visible = false;
}
pub fn handle_key(&mut self, key: KeyEvent) -> Option<bool> {
if !self.visible {
return None;
}
match key.code {
KeyCode::Left | KeyCode::Char('h') => {
self.selected_yes = true;
None
}
KeyCode::Right | KeyCode::Char('l') => {
self.selected_yes = false;
None
}
KeyCode::Char('y') => {
self.hide();
Some(true)
}
KeyCode::Char('n') | KeyCode::Esc => {
self.hide();
Some(false)
}
KeyCode::Enter => {
let result = self.selected_yes;
self.hide();
Some(result)
}
_ => None,
}
}
pub fn render(&self, frame: &mut Frame) {
if !self.visible {
return;
}
let area = centered_rect(40, 20, frame.area());
frame.render_widget(Clear, area);
let yes_style = if self.selected_yes {
Theme::highlight()
} else {
Theme::dim()
};
let no_style = if self.selected_yes {
Theme::dim()
} else {
Theme::highlight()
};
let lines = vec![
Line::from(""),
Line::from(Span::styled(&self.message, Theme::base())),
Line::from(""),
Line::from(vec![
Span::styled(" [ Yes ] ", yes_style),
Span::raw(" "),
Span::styled(" [ No ] ", no_style),
]),
];
let block = Block::default()
.title(format!(" {} ", self.title))
.title_style(Theme::title())
.borders(Borders::ALL)
.border_style(Theme::dim())
.style(Theme::surface());
let paragraph =
Paragraph::new(lines).block(block).alignment(ratatui::layout::Alignment::Center);
frame.render_widget(paragraph, area);
}
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)])
.flex(Flex::Center)
.split(area);
Layout::horizontal([Constraint::Percentage(percent_x)])
.flex(Flex::Center)
.split(vertical[0])[0]
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
fn key(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn new_creates_hidden_dialog() {
let d = ConfirmDialog::new();
assert!(!d.visible);
assert!(d.title.is_empty());
assert!(d.message.is_empty());
assert!(!d.selected_yes);
}
#[test]
fn show_makes_dialog_visible() {
let mut d = ConfirmDialog::new();
d.show("Delete?", "Are you sure you want to delete this?");
assert!(d.visible);
assert_eq!(d.title, "Delete?");
assert_eq!(d.message, "Are you sure you want to delete this?");
assert!(!d.selected_yes);
}
#[test]
fn hide_makes_dialog_invisible() {
let mut d = ConfirmDialog::new();
d.show("Title", "Message");
d.hide();
assert!(!d.visible);
}
#[test]
fn hidden_dialog_does_not_consume_keys() {
let mut d = ConfirmDialog::new();
let result = d.handle_key(key(KeyCode::Char('y')));
assert_eq!(result, None);
}
#[test]
fn y_key_confirms_and_hides() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
let result = d.handle_key(key(KeyCode::Char('y')));
assert_eq!(result, Some(true));
assert!(!d.visible);
}
#[test]
fn n_key_rejects_and_hides() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
let result = d.handle_key(key(KeyCode::Char('n')));
assert_eq!(result, Some(false));
assert!(!d.visible);
}
#[test]
fn esc_key_rejects_and_hides() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
let result = d.handle_key(key(KeyCode::Esc));
assert_eq!(result, Some(false));
assert!(!d.visible);
}
#[test]
fn left_right_toggle_selection() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
assert!(!d.selected_yes);
let result = d.handle_key(key(KeyCode::Left));
assert_eq!(result, None); assert!(d.selected_yes);
let result = d.handle_key(key(KeyCode::Right));
assert_eq!(result, None);
assert!(!d.selected_yes);
}
#[test]
fn h_l_toggle_selection() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
let result = d.handle_key(key(KeyCode::Char('h')));
assert_eq!(result, None);
assert!(d.selected_yes);
let result = d.handle_key(key(KeyCode::Char('l')));
assert_eq!(result, None);
assert!(!d.selected_yes);
}
#[test]
fn enter_confirms_current_selection_yes() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
d.handle_key(key(KeyCode::Left));
assert!(d.selected_yes);
let result = d.handle_key(key(KeyCode::Enter));
assert_eq!(result, Some(true));
assert!(!d.visible);
}
#[test]
fn enter_confirms_current_selection_no() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
assert!(!d.selected_yes);
let result = d.handle_key(key(KeyCode::Enter));
assert_eq!(result, Some(false));
assert!(!d.visible);
}
#[test]
fn unrecognized_key_returns_none() {
let mut d = ConfirmDialog::new();
d.show("Title", "Msg");
let result = d.handle_key(key(KeyCode::Char('x')));
assert_eq!(result, None);
assert!(d.visible);
}
#[test]
fn show_resets_selection_to_no() {
let mut d = ConfirmDialog::new();
d.show("First", "First message");
d.handle_key(key(KeyCode::Left));
assert!(d.selected_yes);
d.show("Second", "Second message");
assert!(!d.selected_yes);
assert_eq!(d.title, "Second");
}
}