embers-client 0.1.0

Client rendering, input handling, configuration, and scripting support for Embers.
use thiserror::Error;

pub type KeySequence = Vec<KeyToken>;

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum KeyToken {
    Char(char),
    Ctrl(char),
    Alt(char),
    Enter,
    Escape,
    Backspace,
    Tab,
    Space,
    Leader,
    Up,
    Down,
    Left,
    Right,
    Home,
    End,
    Insert,
    Delete,
    PageUp,
    PageDown,
}

#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum KeyParseError {
    #[error("key sequence cannot be empty")]
    EmptySequence,
    #[error("key token '<{token}>' is invalid")]
    InvalidToken { token: String },
    #[error("key modifier in '<{token}>' is invalid")]
    InvalidModifier { token: String },
    #[error("key token '<{token}>' must contain exactly one character after the modifier")]
    InvalidModifiedKey { token: String },
    #[error("key sequence '{notation}' has an unterminated token")]
    UnterminatedToken { notation: String },
    #[error("'<leader>' cannot be used before a leader is configured")]
    MissingLeader,
}

pub fn parse_key_sequence(notation: &str) -> Result<KeySequence, KeyParseError> {
    if notation.is_empty() {
        return Err(KeyParseError::EmptySequence);
    }

    let mut sequence = Vec::new();
    let chars = notation.chars().collect::<Vec<_>>();
    let mut index = 0;
    while index < chars.len() {
        match chars[index] {
            '<' => {
                let mut end = index + 1;
                while end < chars.len() && chars[end] != '>' {
                    end += 1;
                }
                if end >= chars.len() {
                    return Err(KeyParseError::UnterminatedToken {
                        notation: notation.to_owned(),
                    });
                }

                let token = chars[index + 1..end].iter().collect::<String>();
                sequence.push(parse_token(&token)?);
                index = end + 1;
            }
            ' ' => {
                sequence.push(KeyToken::Space);
                index += 1;
            }
            ch => {
                sequence.push(KeyToken::Char(ch));
                index += 1;
            }
        }
    }

    Ok(sequence)
}

pub fn expand_leader(
    sequence: impl IntoIterator<Item = KeyToken>,
    leader: &[KeyToken],
) -> Result<KeySequence, KeyParseError> {
    let mut expanded = Vec::new();
    for token in sequence {
        if token == KeyToken::Leader {
            if leader.is_empty() {
                return Err(KeyParseError::MissingLeader);
            }
            expanded.extend(leader.iter().cloned());
        } else {
            expanded.push(token);
        }
    }
    Ok(expanded)
}

fn parse_token(token: &str) -> Result<KeyToken, KeyParseError> {
    let lower = token.to_ascii_lowercase();
    match lower.as_str() {
        "leader" => Ok(KeyToken::Leader),
        "enter" | "return" | "cr" => Ok(KeyToken::Enter),
        "esc" | "escape" => Ok(KeyToken::Escape),
        "bs" | "backspace" => Ok(KeyToken::Backspace),
        "tab" => Ok(KeyToken::Tab),
        "space" => Ok(KeyToken::Space),
        "up" => Ok(KeyToken::Up),
        "down" => Ok(KeyToken::Down),
        "left" => Ok(KeyToken::Left),
        "right" => Ok(KeyToken::Right),
        "home" => Ok(KeyToken::Home),
        "end" => Ok(KeyToken::End),
        "ins" | "insert" => Ok(KeyToken::Insert),
        "del" | "delete" => Ok(KeyToken::Delete),
        "pageup" | "pgup" => Ok(KeyToken::PageUp),
        "pagedown" | "pgdown" | "pgdn" => Ok(KeyToken::PageDown),
        _ => parse_modified_token(token),
    }
}

fn parse_modified_token(token: &str) -> Result<KeyToken, KeyParseError> {
    let Some((modifier, key)) = token.split_once('-') else {
        return single_char_token(token).map(KeyToken::Char).ok_or_else(|| {
            KeyParseError::InvalidToken {
                token: token.to_owned(),
            }
        });
    };

    let ch = single_char_token(key).ok_or_else(|| KeyParseError::InvalidModifiedKey {
        token: token.to_owned(),
    })?;

    match modifier.to_ascii_lowercase().as_str() {
        "c" | "ctrl" => Ok(KeyToken::Ctrl(ch.to_ascii_lowercase())),
        "a" | "alt" | "m" => Ok(KeyToken::Alt(ch.to_ascii_lowercase())),
        _ => Err(KeyParseError::InvalidModifier {
            token: token.to_owned(),
        }),
    }
}

fn single_char_token(token: &str) -> Option<char> {
    let mut chars = token.chars();
    let ch = chars.next()?;
    chars.next().is_none().then_some(ch)
}

#[cfg(test)]
mod tests {
    use super::{KeyParseError, KeyToken, expand_leader, parse_key_sequence};

    #[test]
    fn parses_plain_and_modified_keys() {
        assert_eq!(
            parse_key_sequence(
                "ab<C-x><A-z><Enter><Esc><Tab><Space><Home><Insert><Delete><End><Up><PageDown>",
            )
            .unwrap(),
            vec![
                KeyToken::Char('a'),
                KeyToken::Char('b'),
                KeyToken::Ctrl('x'),
                KeyToken::Alt('z'),
                KeyToken::Enter,
                KeyToken::Escape,
                KeyToken::Tab,
                KeyToken::Space,
                KeyToken::Home,
                KeyToken::Insert,
                KeyToken::Delete,
                KeyToken::End,
                KeyToken::Up,
                KeyToken::PageDown,
            ]
        );
    }

    #[test]
    fn rejects_invalid_tokens() {
        assert_eq!(
            parse_key_sequence("<Hyper-x>").unwrap_err(),
            KeyParseError::InvalidModifier {
                token: "Hyper-x".to_owned(),
            }
        );
        assert_eq!(
            parse_key_sequence("<C-ab>").unwrap_err(),
            KeyParseError::InvalidModifiedKey {
                token: "C-ab".to_owned(),
            }
        );
    }

    #[test]
    fn expands_leader_tokens() {
        let sequence = parse_key_sequence("<leader>ws").unwrap();
        let leader = parse_key_sequence("<C-a>").unwrap();

        assert_eq!(
            expand_leader(sequence, &leader).unwrap(),
            vec![
                KeyToken::Ctrl('a'),
                KeyToken::Char('w'),
                KeyToken::Char('s'),
            ]
        );
    }

    #[test]
    fn leader_expansion_requires_configured_leader() {
        let sequence = parse_key_sequence("<leader>x").unwrap();
        assert_eq!(
            expand_leader(sequence, &[]).unwrap_err(),
            KeyParseError::MissingLeader
        );
    }
}