leadr 2.8.6

Shell aliases on steroids
Documentation
use color_eyre::eyre::{Result, WrapErr, eyre};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

/// Parses a full Vim-style sequence like `<C-x><M-Enter>` into a vector of KeyEvents
pub fn parse_keysequence(seq: &str) -> Result<Vec<KeyEvent>> {
    let mut result = Vec::new();
    let mut current_combo = String::new();
    let mut in_angle = false;

    for char in seq.chars() {
        if char == '<' {
            in_angle = true;
            current_combo.push(char);
        } else if char == '>' && in_angle {
            current_combo.push(char);
            in_angle = false;
            let key_event = parse_vim_key(&current_combo)?;
            result.push(key_event);
            current_combo.clear();
        } else if in_angle {
            current_combo.push(char);
        } else {
            // plain character
            result.push(KeyEvent {
                code: KeyCode::Char(char),
                modifiers: KeyModifiers::empty(),
                kind: crossterm::event::KeyEventKind::Press,
                state: crossterm::event::KeyEventState::NONE,
            });
        }
    }

    Ok(result)
}

/// Parses a single Vim-style key like `<C-x>`, `<M-Enter>`, `<F5>`.
fn parse_vim_key(key: &str) -> Result<KeyEvent> {
    let key = key.trim_matches(|c| c == '<' || c == '>');
    let parts: Vec<&str> = key.split('-').collect();

    let mut ctrl = false;
    let mut alt = false;
    let mut shift = false;
    let base_key;

    if parts.len() > 1 {
        // Treat first N-1 as modifiers, last as base key
        for part in &parts[..parts.len() - 1] {
            match part.to_uppercase().as_str() {
                "C" => ctrl = true,
                "M" => alt = true,
                "S" => shift = true,
                other => {
                    return Err(eyre!(
                        "Invalid leadr keymap: <{key}>.\n  \
                        '{other}' is not a recognized modifier (valid modifiers are C, M, S)."
                    ));
                }
            }
        }
        base_key = parts[parts.len() - 1].to_string();
    } else {
        // Single element: literal
        base_key = parts[0].to_string();
    }

    let code = match base_key {
        k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()),
        k => match k.to_uppercase().as_str() {
            "SPACE" => KeyCode::Char(' '),
            "CR" | "ENTER" => KeyCode::Enter,
            "TAB" => KeyCode::Tab,
            "ESC" => KeyCode::Esc,
            "UP" => KeyCode::Up,
            "DOWN" => KeyCode::Down,
            "LEFT" => KeyCode::Left,
            "RIGHT" => KeyCode::Right,
            k if k.starts_with('F') => {
                let n = k[1..].parse::<u8>().wrap_err_with(|| {
                    format!("Invalid leadr keymap: <{key}>. '{k}' is not a valid function key.")
                })?;
                KeyCode::F(n)
            }
            _ => {
                return Err(eyre!(
                    "Invalid leadr keymap: <{key}>. '{k}' is not a recognized keycode."
                ));
            }
        },
    };

    let mut modifiers = KeyModifiers::empty();
    if ctrl {
        modifiers |= KeyModifiers::CONTROL;
    }
    if alt {
        modifiers |= KeyModifiers::ALT;
    }
    if shift {
        modifiers |= KeyModifiers::SHIFT;
    }

    Ok(KeyEvent {
        code,
        modifiers,
        kind: crossterm::event::KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    })
}

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

    #[test]
    fn test_parse_simple_char() {
        let ev = parse_vim_key("x").unwrap();
        assert_eq!(ev.code, KeyCode::Char('x'));
        assert!(ev.modifiers.is_empty());
    }

    #[test]
    fn test_parse_ctrl_char() {
        let ev = parse_vim_key("<C-a>").unwrap();
        assert_eq!(ev.code, KeyCode::Char('a'));
        assert!(ev.modifiers.contains(KeyModifiers::CONTROL));
    }

    #[test]
    fn test_parse_alt_shift_char() {
        let ev = parse_vim_key("<M-S-x>").unwrap();
        assert_eq!(ev.code, KeyCode::Char("x".chars().next().unwrap()));
        assert!(ev.modifiers.contains(KeyModifiers::ALT));
        assert!(ev.modifiers.contains(KeyModifiers::SHIFT));
    }

    #[test]
    fn test_parse_named_keys() {
        assert_eq!(parse_vim_key("<Enter>").unwrap().code, KeyCode::Enter);
        assert_eq!(parse_vim_key("<Tab>").unwrap().code, KeyCode::Tab);
        assert_eq!(parse_vim_key("<Esc>").unwrap().code, KeyCode::Esc);
        assert_eq!(parse_vim_key("<Up>").unwrap().code, KeyCode::Up);
        assert_eq!(parse_vim_key("<Down>").unwrap().code, KeyCode::Down);
        assert_eq!(parse_vim_key("<Left>").unwrap().code, KeyCode::Left);
        assert_eq!(parse_vim_key("<Right>").unwrap().code, KeyCode::Right);
    }

    #[test]
    fn test_parse_function_keys() {
        assert_eq!(parse_vim_key("<F1>").unwrap().code, KeyCode::F(1));
        assert_eq!(parse_vim_key("<F12>").unwrap().code, KeyCode::F(12));
        assert!(parse_vim_key("<F13>").is_ok()); // still parsed, but no mapping in keyevent_to_shell_seq
    }

    #[test]
    fn test_invalid_modifier() {
        let err = parse_vim_key("<Q-x>").unwrap_err();
        let msg = format!("{err}"); // get the error message
        assert!(
            msg.contains("not a recognized modifier"),
            "Error message: {msg}"
        );
    }

    #[test]
    fn test_invalid_key() {
        let err = parse_vim_key("<C-NotAKey>").unwrap_err();
        let msg = format!("{err}"); // get the error message
        assert!(
            msg.contains("not a recognized keycode"),
            "Error message: {msg}"
        );
    }

    #[test]
    fn test_parse_keysequence_simple() {
        let events = parse_keysequence("abc").unwrap();
        assert_eq!(events.len(), 3);
        assert_eq!(events[0].code, KeyCode::Char('a'));
        assert_eq!(events[1].code, KeyCode::Char('b'));
        assert_eq!(events[2].code, KeyCode::Char('c'));
    }

    #[test]
    fn test_parse_keysequence_vim_style() {
        let events = parse_keysequence("<C-x><M-Enter>").unwrap();
        assert_eq!(events.len(), 2);
        assert_eq!(events[0].code, KeyCode::Char('x'));
        assert!(events[0].modifiers.contains(KeyModifiers::CONTROL));
        assert_eq!(events[1].code, KeyCode::Enter);
        assert!(events[1].modifiers.contains(KeyModifiers::ALT));
    }
}