use crossterm::{
event::{
read, DisableMouseCapture, EnableMouseCapture, Event,
KeyModifiers, MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Layout, Rect},
text::Line,
widgets::Widget,
Terminal,
};
use hefesto_widgets::{ConfirmationVariant, HefestoConfirmationPopup};
use crate::{keybinds, style};
const POPUP_WIDTH: u16 = 44;
const POPUP_HEIGHT: u16 = 9;
const CONFIRM_GUIDE: &str = include_str!("../CONFIRM_GUIDE.md");
#[derive(clap::Args)]
pub struct ConfirmArgs {
#[arg(short, long, default_value = "¿Deseas continuar?")]
pub message: String,
#[arg(short, long, default_value = "Confirmación")]
pub title: String,
#[arg(short, long, default_value = "warning")]
pub variant: String,
#[arg(short = 'y', long, default_value = "Confirmar")]
pub confirm: String,
#[arg(short = 'n', long, default_value = "Cancelar")]
pub cancel: String,
#[arg(short = 'W', long, default_value = "0")]
pub width: u16,
#[arg(short = 'H', long, default_value = "0")]
pub height: u16,
#[arg(long)]
pub guide: bool,
}
fn parse_variant(s: &str) -> ConfirmationVariant {
match s.to_lowercase().as_str() {
"success" => ConfirmationVariant::Success,
"warning" => ConfirmationVariant::Warning,
"danger" => ConfirmationVariant::Danger,
_ => ConfirmationVariant::None,
}
}
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
Rect {
x: (area.width.saturating_sub(width)) / 2,
y: (area.height.saturating_sub(height)) / 2,
width: width.min(area.width),
height: height.min(area.height),
}
}
fn button_rects(popup: Rect) -> (Rect, Rect) {
let inner = Rect {
x: popup.x + 1,
y: popup.y + 1,
width: popup.width.saturating_sub(2),
height: popup.height.saturating_sub(2),
};
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(2),
])
.split(inner);
let options = chunks[2];
let options_inner = Rect {
x: options.x + 1,
y: options.y + 1,
width: options.width.saturating_sub(2),
height: options.height.saturating_sub(1),
};
let horiz = Layout::horizontal([
Constraint::Percentage(48),
Constraint::Length(1),
Constraint::Percentage(48),
])
.split(options_inner);
(horiz[0], horiz[2])
}
fn contains(rect: Rect, col: u16, row: u16) -> bool {
col >= rect.x
&& col < rect.x + rect.width
&& row >= rect.y
&& row < rect.y + rect.height
}
fn title_rect(popup: Rect) -> Rect {
let inner = Rect {
x: popup.x + 1,
y: popup.y + 1,
width: popup.width.saturating_sub(2),
height: popup.height.saturating_sub(2),
};
Rect {
x: inner.x,
y: inner.y,
width: inner.width,
height: 2,
}
}
fn is_drag_area(popup: Rect, col: u16, row: u16) -> bool {
contains(popup, col, row)
&& (is_on_border(popup, col, row) || contains(title_rect(popup), col, row))
}
fn is_on_border(popup: Rect, col: u16, row: u16) -> bool {
if !contains(popup, col, row) {
return false;
}
let inner = Rect {
x: popup.x + 1,
y: popup.y + 1,
width: popup.width.saturating_sub(2),
height: popup.height.saturating_sub(2),
};
!contains(inner, col, row)
}
pub fn run(args: ConfirmArgs) {
if args.guide {
println!("{}", CONFIRM_GUIDE);
return;
}
let variant = parse_variant(&args.variant);
let mut tty: Box<dyn std::io::Write> = match std::fs::OpenOptions::new().write(true).open("/dev/tty") {
Ok(f) => Box::new(f),
Err(_) => Box::new(std::io::stdout()),
};
enable_raw_mode().unwrap();
execute!(tty, EnterAlternateScreen, EnableMouseCapture).unwrap();
let mut terminal = Terminal::new(CrosstermBackend::new(tty)).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
let title = args.title.as_str();
let body = vec![Line::from(args.message.as_str())];
let mut drag_offset: Option<(u16, u16)> = None;
let mut popup_rect: Option<Rect> = None;
let mut result = None;
while result.is_none() {
let size = terminal.size().unwrap();
let area = Rect::new(0, 0, size.width, size.height);
let pw = if args.width > 0 { args.width } else { POPUP_WIDTH };
let ph = if args.height > 0 { args.height } else { POPUP_HEIGHT };
if popup_rect.is_none() {
popup_rect = Some(centered_rect(area, pw, ph));
}
let pr = popup_rect.unwrap();
let (confirm_rect, cancel_rect) = button_rects(pr);
let popup = HefestoConfirmationPopup::new(title, body.clone(), variant)
.border_type(style::BORDER)
.options(&args.confirm, &args.cancel)
.position(pr);
terminal
.draw(|frame| {
let area = frame.area();
popup.clone().render(area, frame.buffer_mut());
})
.unwrap();
match read().unwrap() {
Event::Key(key) => {
if key.code == keybinds::EMERGENCY && key.modifiers == KeyModifiers::CONTROL {
result = Some(1);
} else {
match key.code {
keybinds::CONFIRM_YES | keybinds::CONFIRM_YES_UPPER | keybinds::CONFIRM => result = Some(0),
keybinds::CONFIRM_NO | keybinds::CONFIRM_NO_UPPER | keybinds::CANCEL => result = Some(1),
_ => {}
}
}
},
Event::Mouse(mouse) => {
let col = mouse.column;
let row = mouse.row;
match mouse.kind {
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
if contains(confirm_rect, col, row) {
result = Some(0);
} else if contains(cancel_rect, col, row) {
result = Some(1);
} else if is_drag_area(pr, col, row) {
let ox = col.saturating_sub(pr.x);
let oy = row.saturating_sub(pr.y);
drag_offset = Some((ox, oy));
}
}
MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
if let Some((ox, oy)) = drag_offset {
let max_w = area.width.saturating_sub(POPUP_WIDTH);
let max_h = area.height.saturating_sub(POPUP_HEIGHT);
let nx = (col as i16 - ox as i16).clamp(0, max_w as i16) as u16;
let ny = (row as i16 - oy as i16).clamp(0, max_h as i16) as u16;
popup_rect = Some(Rect::new(nx, ny, POPUP_WIDTH, POPUP_HEIGHT));
}
}
MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
drag_offset = None;
}
_ => {}
}
}
_ => {}
}
}
let code = result.unwrap();
disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
terminal.show_cursor().unwrap();
std::process::exit(code);
}