pandora-kit 0.3.0

Interactive TUI toolkit for the Hefesto framework
use clap::Args;
use crossterm::{
    event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    widgets::StatefulWidget,
    Terminal,
};
use hefesto_widgets::{TextInputPopup, TextInputState};

use crate::{keybinds, style};

const INPUT_GUIDE: &str = include_str!("../INPUT_GUIDE.md");

#[derive(Args)]
pub struct InputArgs {
    /// Prompt message
    #[arg(short, long, default_value = "Input")]
    pub prompt: String,

    /// Popup width (0 = auto)
    #[arg(short = 'W', long, default_value = "0")]
    pub width: u16,

    /// Initial value
    #[arg(short = 'v', long, default_value = "")]
    pub value: String,

    #[arg(long)]
    pub guide: bool,
}

pub fn run(args: InputArgs) {
    if args.guide {
        println!("{}", INPUT_GUIDE);
        return;
    }

    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).is_err() {
        eprintln!("pandora input: el terminal no es interactivo");
        std::process::exit(1);
    }
    let mut terminal = Terminal::new(CrosstermBackend::new(tty)).unwrap();
    terminal.clear().unwrap();
    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;

    while result.is_none() {
        let popup = TextInputPopup::new()
            .title(&prompt)
            .text_style(ratatui::style::Style::new().fg(style::TEXT))
            .cursor_style(ratatui::style::Style::new().fg(style::ACCENT))
            .fill_bg(style::FILL);

        terminal
            .draw(|frame| {
                StatefulWidget::render(popup, 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 | keybinds::CANCEL_ALT => {
                        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) => {
                if mouse.kind == MouseEventKind::Down(crossterm::event::MouseButton::Left) {
                    // no mouse handling needed
                }
            }
            _ => {}
        }
    }

    disable_raw_mode().unwrap();
    execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture).unwrap();
    terminal.show_cursor().unwrap();

    if let Some(text) = result {
        println!("{}", text);
        std::process::exit(0);
    }
    std::process::exit(1);
}