sdd-layer 0.13.0

Spec-Driven Development CLI and agent harness
//! Mapeamento de eventos de teclado para ações da TUI (T-06, RF-10).

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
    Quit,
    Confirm,
    Back,
    Up,
    Down,
    PreviousProvider,
    NextProvider,
    PreviousModel,
    NextModel,
    PreviousEffort,
    NextEffort,
    New,
    Approve,
    Regenerate,
    Edit,
    Skip,
    Provider,
    Help,
    OpenPalette,
    OpenSlashMenu,
    Telemetry,
    Char(char),
    Backspace,
    None,
}

/// Traduz um KeyEvent em Action. Em telas de input de texto, `text_mode = true`
/// faz com que caracteres imprimíveis virem `Char` em vez de atalhos.
pub fn map_key(key: KeyEvent, text_mode: bool) -> Action {
    // Ctrl+C sempre encerra; Ctrl+N alterna rapidamente o modelo sem sair do fluxo.
    if key.modifiers.contains(KeyModifiers::CONTROL) {
        match key.code {
            KeyCode::Char('c') => return Action::Quit,
            KeyCode::Char('n') => return Action::NextModel,
            // Ctrl+K abre o command palette em qualquer contexto (inclusive text_mode).
            KeyCode::Char('k') => return Action::OpenPalette,
            _ => {}
        }
    }

    match key.code {
        KeyCode::Esc => Action::Back,
        KeyCode::Enter => Action::Confirm,
        KeyCode::Up => Action::Up,
        KeyCode::Down => Action::Down,
        KeyCode::Left => Action::PreviousModel,
        KeyCode::Right => Action::NextModel,
        KeyCode::Tab => Action::NextProvider,
        KeyCode::BackTab => Action::PreviousProvider,
        KeyCode::Char('/') => Action::OpenSlashMenu,
        KeyCode::Backspace if text_mode => Action::Backspace,
        KeyCode::Char(c) if text_mode => Action::Char(c),
        // Fora de text_mode: "/" abre o menu de comandos e "t" alterna a telemetria.
        KeyCode::Char('t') => Action::Telemetry,
        KeyCode::Char('q') => Action::Quit,
        KeyCode::Char('j') => Action::Down,
        KeyCode::Char('k') => Action::Up,
        KeyCode::Char('n') => Action::New,
        KeyCode::Char('a') => Action::Approve,
        KeyCode::Char('r') => Action::Regenerate,
        KeyCode::Char('e') => Action::Edit,
        KeyCode::Char('s') => Action::Skip,
        KeyCode::Char('p') => Action::Provider,
        KeyCode::Char('[') => Action::PreviousEffort,
        KeyCode::Char(']') => Action::NextEffort,
        KeyCode::Char('?') => Action::Help,
        _ => Action::None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn ev(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    #[test]
    fn ctrl_c_quits() {
        let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
        assert_eq!(map_key(k, false), Action::Quit);
    }

    #[test]
    fn q_quits_in_nav_mode() {
        assert_eq!(map_key(ev(KeyCode::Char('q')), false), Action::Quit);
    }

    #[test]
    fn char_in_text_mode_is_char() {
        assert_eq!(map_key(ev(KeyCode::Char('q')), true), Action::Char('q'));
    }

    #[test]
    fn shortcuts_map() {
        assert_eq!(map_key(ev(KeyCode::Char('a')), false), Action::Approve);
        assert_eq!(map_key(ev(KeyCode::Char('r')), false), Action::Regenerate);
        assert_eq!(map_key(ev(KeyCode::Char('e')), false), Action::Edit);
        assert_eq!(map_key(ev(KeyCode::Char('p')), false), Action::Provider);
        assert_eq!(map_key(ev(KeyCode::Char('?')), false), Action::Help);
        assert_eq!(map_key(ev(KeyCode::Up), false), Action::Up);
        assert_eq!(map_key(ev(KeyCode::Tab), false), Action::NextProvider);
        assert_eq!(
            map_key(ev(KeyCode::BackTab), false),
            Action::PreviousProvider
        );
        assert_eq!(map_key(ev(KeyCode::Left), false), Action::PreviousModel);
        assert_eq!(map_key(ev(KeyCode::Right), false), Action::NextModel);
        assert_eq!(
            map_key(
                KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL),
                false
            ),
            Action::NextModel
        );
        assert_eq!(
            map_key(ev(KeyCode::Char('[')), false),
            Action::PreviousEffort
        );
        assert_eq!(map_key(ev(KeyCode::Char(']')), false), Action::NextEffort);
        assert_eq!(map_key(ev(KeyCode::Esc), false), Action::Back);
    }

    #[test]
    fn ctrl_k_opens_palette() {
        let k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
        assert_eq!(map_key(k, false), Action::OpenPalette);
        // Ctrl+K vale inclusive em text_mode.
        assert_eq!(map_key(k, true), Action::OpenPalette);
    }

    #[test]
    fn slash_opens_menu_even_in_text_mode_and_t_is_contextual() {
        assert_eq!(
            map_key(ev(KeyCode::Char('/')), false),
            Action::OpenSlashMenu
        );
        assert_eq!(map_key(ev(KeyCode::Char('/')), true), Action::OpenSlashMenu);
        assert_eq!(map_key(ev(KeyCode::Char('t')), false), Action::Telemetry);
        // Em text_mode, t continua sendo texto comum.
        assert_eq!(map_key(ev(KeyCode::Char('t')), true), Action::Char('t'));
    }
}