use clap::Args;
use crossterm::{
event::{read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::Modifier,
widgets::StatefulWidget,
Terminal,
};
use hefesto_widgets::{TextInputPopup, TextInputState};
use crate::popup_config::{PopupConfig, PopupConfigurable};
use crate::{badge, keybinds, style};
const INPUT_GUIDE: &str = include_str!("../guides/INPUT_GUIDE.md");
#[derive(Args)]
pub struct InputArgs {
#[arg(short = 't', long, default_value = "Introduzca")]
pub prompt: String,
#[arg(short = 'W', long, default_value = "0")]
pub width: u16,
#[arg(short = 'v', long, default_value = "")]
pub value: String,
#[command(flatten)]
pub badge: badge::BadgeArgs,
#[arg(long)]
pub guide: bool,
}
pub fn run(args: InputArgs) {
if args.guide {
println!("{}", INPUT_GUIDE);
return;
}
crate::tty::ensure_terminal_stdin();
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()),
};
if enable_raw_mode().is_err() || execute!(tty, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste).is_err() {
eprintln!("{} input: el terminal no es interactivo", crate::BIN_NAME);
std::process::exit(1);
}
let mut terminal = Terminal::new(CrosstermBackend::new(tty)).unwrap();
crate::tty::with_terminal_stdout(|| terminal.clear()).unwrap();
crate::tty::with_terminal_stdout(|| terminal.hide_cursor()).unwrap();
let prompt = args.prompt.as_str();
let mut state = TextInputState {
content: args.value.clone(),
cursor: args.value.len(),
};
let mut result: Option<String> = None;
let mut drag = crate::drag::DragState::new();
let mut origin: Option<(u16, u16)> = None;
let mut resize_w: Option<u16> = None;
let mut resize_h: Option<u16> = None;
let mut cfg = PopupConfig::new()
.header();
if args.width > 0 { cfg = cfg.width(args.width); }
let badge_specs = args.badge.prepare_specs();
let badge_stack = if badge_specs.is_empty() { None } else { Some(badge::build_badge_stack(&badge_specs)) };
while result.is_none() {
let size = terminal.size().unwrap();
let area = Rect::new(0, 0, size.width, size.height);
let mut popup = TextInputPopup::new()
.title(&prompt)
.text_style(ratatui::style::Style::new().fg(style::TEXT))
.cursor_style(ratatui::style::Style::new().fg(style::ACCENT).add_modifier(Modifier::REVERSED))
.with_config(&cfg);
if let Some((ox, oy)) = origin {
popup = popup.origin(ox, oy);
}
if let Some(w) = resize_w { popup = popup.width(hefesto_widgets::PopupSize::Fixed(w)); }
if let Some(h) = resize_h { popup = popup.height(hefesto_widgets::PopupSize::Fixed(h)); }
let pr = popup.resolve_rect(area, &state);
if resize_w.is_some() || resize_h.is_some() {
resize_w = Some(pr.width);
resize_h = Some(pr.height);
}
if let Some(ref stack) = badge_stack {
popup = popup.badges(stack.clone());
}
terminal
.draw(|frame| {
StatefulWidget::render(popup.clone(), frame.area(), frame.buffer_mut(), &mut state);
})
.unwrap();
match read().unwrap() {
Event::Key(key) => {
if key.code == keybinds::EMERGENCY && key.modifiers == KeyModifiers::CONTROL {
break;
}
match key.code {
keybinds::CONFIRM => {
result = Some(state.content.clone());
}
keybinds::CANCEL => {
break;
}
keybinds::FILTER_BACKSPACE => state.delete_before(),
keybinds::FILTER_LEFT => state.cursor_left(),
keybinds::FILTER_RIGHT => state.cursor_right(),
keybinds::FILTER_HOME => state.cursor_home(),
keybinds::FILTER_END => state.cursor_end(),
KeyCode::Char(c) => state.insert_char(c),
_ => {}
}
}
Event::Mouse(mouse) => {
let col = mouse.column;
let row = mouse.row;
match mouse.kind {
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
let zone = crate::drag::default_zone_at(pr, col, row);
drag.begin(pr, col, row, zone);
}
MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
match drag.update(area, col, row) {
crate::drag::DragUpdate::Moved { x, y } => origin = Some((x, y)),
crate::drag::DragUpdate::Resized { x, y, w, h } => {
origin = Some((x, y));
resize_w = Some(w);
resize_h = Some(h);
}
crate::drag::DragUpdate::None => {}
}
}
MouseEventKind::Up(crossterm::event::MouseButton::Left) => {
drag.end();
}
_ => {}
}
}
Event::Paste(text) => {
for c in text.chars() {
state.insert_char(c);
}
}
_ => {}
}
}
disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture, DisableBracketedPaste).unwrap();
terminal.show_cursor().unwrap();
if let Some(text) = result {
println!("{}", text);
std::process::exit(0);
}
std::process::exit(1);
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
use ratatui::{backend::TestBackend, style::Color, Terminal};
fn render_popup(name: &str, popup: TextInputPopup<'_>, state: &mut TextInputState) {
let backend = TestBackend::new(60, 20);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| f.render_stateful_widget(popup, f.area(), state))
.unwrap();
insta::with_settings!({
snapshot_path => "input/snapshots",
prepend_module_to_snapshot => false,
}, {
assert_snapshot!(name, terminal.backend());
});
}
#[test]
fn snapshot_default() {
let popup = TextInputPopup::new().title("Nombre");
let mut state = TextInputState::default();
render_popup("input_default", popup, &mut state);
}
#[test]
fn snapshot_with_value() {
let popup = TextInputPopup::new().title("Nombre");
let mut state = TextInputState {
content: "SpicyDogWings".to_string(),
cursor: 13,
};
render_popup("input_with_value", popup, &mut state);
}
#[test]
fn snapshot_with_placeholder() {
let popup = TextInputPopup::new()
.title("Email")
.placeholder("tu@correo.com");
let mut state = TextInputState::default();
render_popup("input_with_placeholder", popup, &mut state);
}
#[test]
fn snapshot_with_badges() {
let specs = vec![
badge::prepare_badge(
badge::BadgeParts { text: "input".into(), color: Color::Green, anchor: None, width: None, lines: None },
badge::parse_anchor("tl").unwrap(), 1, None,
),
];
let stack = badge::build_badge_stack(&specs);
let mut state = TextInputState::default();
let backend = TestBackend::new(60, 20);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let popup = TextInputPopup::new().title("Nombre").badges(stack.clone());
f.render_stateful_widget(popup, f.area(), &mut state);
})
.unwrap();
insta::with_settings!({
snapshot_path => "input/snapshots",
prepend_module_to_snapshot => false,
}, {
assert_snapshot!("input_with_badges", terminal.backend());
});
}
}