bath 0.4.0

A TUI tool to manage and export environment variable profiles
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use std::time::Duration;

/// Extracts a key *press* from an event, dropping everything else.
///
/// crossterm 0.28 emits `KeyEventKind::Release` (and `Repeat`) events on
/// Windows; handling them would process every keypress twice.
pub fn key_press_filter(ev: Event) -> Option<KeyEvent> {
    match ev {
        Event::Key(key) if key.kind == KeyEventKind::Press => Some(key),
        _ => None,
    }
}

/// Waits up to `timeout` for the next key press.
///
/// Returns `Ok(None)` when there is nothing to handle: timeout expired, a
/// non-key event arrived, or the key event was a release/repeat. Every event
/// loop in the TUI must read keys through this helper so no site processes
/// non-press events.
pub fn next_key_press(timeout: Duration) -> Result<Option<KeyEvent>> {
    if event::poll(timeout)? {
        return Ok(key_press_filter(event::read()?));
    }
    Ok(None)
}

/// True for the Ctrl+C chord (either letter case).
///
/// Every modal dialog loop treats Ctrl+C as cancel (Esc-equivalent), so the
/// global quit chord is never a dead key while a dialog is open.
pub fn is_ctrl_c(key: &KeyEvent) -> bool {
    key.modifiers.contains(KeyModifiers::CONTROL)
        && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
}

pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let popup_layout = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage((100 - percent_y) / 2),
                Constraint::Percentage(percent_y),
                Constraint::Percentage((100 - percent_y) / 2),
            ]
            .as_ref(),
        )
        .split(r);
    let horizontal = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(
            [
                Constraint::Percentage((100 - percent_x) / 2),
                Constraint::Percentage(percent_x),
                Constraint::Percentage((100 - percent_x) / 2),
            ]
            .as_ref(),
        )
        .split(popup_layout[1]);
    horizontal[1]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::{KeyCode, KeyEventKind, KeyEventState, KeyModifiers};

    fn key_event(kind: KeyEventKind) -> Event {
        Event::Key(KeyEvent {
            code: KeyCode::Char('j'),
            modifiers: KeyModifiers::NONE,
            kind,
            state: KeyEventState::NONE,
        })
    }

    #[test]
    fn key_press_filter_accepts_press_events() {
        let got = key_press_filter(key_event(KeyEventKind::Press));
        assert_eq!(got.map(|k| k.code), Some(KeyCode::Char('j')));
    }

    #[test]
    fn key_press_filter_drops_release_and_repeat_events() {
        // crossterm 0.28 emits Release events on Windows; processing them
        // would double every keypress (F13).
        assert!(key_press_filter(key_event(KeyEventKind::Release)).is_none());
        assert!(key_press_filter(key_event(KeyEventKind::Repeat)).is_none());
    }

    #[test]
    fn key_press_filter_ignores_non_key_events() {
        assert!(key_press_filter(Event::Resize(80, 24)).is_none());
    }

    #[test]
    fn is_ctrl_c_matches_the_quit_chord_in_both_cases() {
        assert!(is_ctrl_c(&KeyEvent::new(
            KeyCode::Char('c'),
            KeyModifiers::CONTROL
        )));
        assert!(is_ctrl_c(&KeyEvent::new(
            KeyCode::Char('C'),
            KeyModifiers::CONTROL
        )));
        // Plain 'c' must keep reaching text fields.
        assert!(!is_ctrl_c(&KeyEvent::new(
            KeyCode::Char('c'),
            KeyModifiers::NONE
        )));
        assert!(!is_ctrl_c(&KeyEvent::new(
            KeyCode::Char('x'),
            KeyModifiers::CONTROL
        )));
    }
}