mame 0.3.0

Common building blocks to build TUI applications.
Documentation
use tuinix::{KeyCode, KeyInput, MouseEvent};

/// Matches terminal input (keyboard and mouse) against specific patterns.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum InputMatcher {
    /// Matches an exact key combination
    Key(KeyInput),

    /// Matches any printable character
    Printable,

    /// Matches a specific mouse event
    Mouse(MouseEvent),
}

impl InputMatcher {
    /// Returns `true` if the given terminal input matches this matcher pattern.
    pub fn matches(self, input: tuinix::TerminalInput) -> bool {
        match input {
            tuinix::TerminalInput::Key(key) => match self {
                InputMatcher::Key(k) => k == key,
                InputMatcher::Printable => {
                    if let KeyInput {
                        ctrl: false,
                        alt: false,
                        code: KeyCode::Char(ch),
                    } = key
                    {
                        !ch.is_control()
                    } else {
                        false
                    }
                }
                _ => false,
            },
            tuinix::TerminalInput::Mouse(m) => {
                matches!(self, InputMatcher::Mouse(e) if e == m.event)
            }
        }
    }
}

impl std::str::FromStr for InputMatcher {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Handle inputs that do not accept modifiers
        let mouse = |event| InputMatcher::Mouse(event);
        match s {
            "<PRINTABLE>" => return Ok(InputMatcher::Printable),
            "<LEFTCLICK>" => return Ok(mouse(MouseEvent::LeftPress)),
            "<LEFTRELEASE>" => return Ok(mouse(MouseEvent::LeftRelease)),
            "<RIGHTCLICK>" => return Ok(mouse(MouseEvent::RightPress)),
            "<RIGHTRELEASE>" => return Ok(mouse(MouseEvent::RightRelease)),
            "<MIDDLECLICK>" => return Ok(mouse(MouseEvent::MiddlePress)),
            "<MIDDLERELEASE>" => return Ok(mouse(MouseEvent::MiddleRelease)),
            "<DRAG>" => return Ok(mouse(MouseEvent::Drag)),
            "<SCROLLUP>" => return Ok(mouse(MouseEvent::ScrollUp)),
            "<SCROLLDOWN>" => return Ok(mouse(MouseEvent::ScrollDown)),
            _ => {}
        }

        // Handle modifier key combinations like "C-c", "M-x"
        let mut alt = false;
        let mut ctrl = false;
        let mut remaining = s;

        loop {
            if let Some(rest) = remaining.strip_prefix("M-")
                && !alt
            {
                remaining = rest;
                alt = true;
            } else if let Some(rest) = remaining.strip_prefix("C-")
                && !ctrl
            {
                remaining = rest;
                ctrl = true;
            } else {
                break;
            }
        }

        // Handle special keys
        let key = |code| InputMatcher::Key(KeyInput { ctrl, alt, code });
        match remaining {
            "<UP>" => return Ok(key(KeyCode::Up)),
            "<DOWN>" => return Ok(key(KeyCode::Down)),
            "<LEFT>" => return Ok(key(KeyCode::Left)),
            "<RIGHT>" => return Ok(key(KeyCode::Right)),
            "<ENTER>" => return Ok(key(KeyCode::Enter)),
            "<ESCAPE>" => return Ok(key(KeyCode::Escape)),
            "<BACKSPACE>" => return Ok(key(KeyCode::Backspace)),
            "<TAB>" => return Ok(key(KeyCode::Tab)),
            "<BACKTAB>" => return Ok(key(KeyCode::BackTab)),
            "<DELETE>" => return Ok(key(KeyCode::Delete)),
            "<INSERT>" => return Ok(key(KeyCode::Insert)),
            "<HOME>" => return Ok(key(KeyCode::Home)),
            "<END>" => return Ok(key(KeyCode::End)),
            "<PAGEUP>" => return Ok(key(KeyCode::PageUp)),
            "<PAGEDOWN>" => return Ok(key(KeyCode::PageDown)),
            _ => {}
        }

        // Handle character input
        let mut chars = remaining.chars();
        if let Some(ch) = chars.next()
            && chars.next().is_none()
        {
            let code = KeyCode::Char(ch);
            Ok(key(code))
        } else if let Some(hex_str) = remaining.strip_prefix("0x") {
            // Handle hex notation for control chars such as 0x7f
            match u32::from_str_radix(hex_str, 16) {
                Ok(code_point) => {
                    if let Some(ch) = char::from_u32(code_point) {
                        let code = KeyCode::Char(ch);
                        Ok(key(code))
                    } else {
                        Err(format!("invalid Unicode code point: 0x{:x}", code_point))
                    }
                }
                Err(_) => Err(format!("invalid hex notation: {}", remaining)),
            }
        } else {
            Err(format!("invalid key input format: {s:?}"))
        }
    }
}

impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for InputMatcher {
    type Error = nojson::JsonParseError;

    fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
        value
            .to_unquoted_string_str()?
            .parse()
            .map_err(|e| value.invalid(e))
    }
}

impl std::fmt::Display for InputMatcher {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Printable => write!(f, "<PRINTABLE>"),
            Self::Key(key) => {
                if key.alt {
                    write!(f, "M-")?;
                }
                if key.ctrl {
                    write!(f, "C-")?;
                }

                match key.code {
                    KeyCode::Up => write!(f, "<UP>"),
                    KeyCode::Down => write!(f, "<DOWN>"),
                    KeyCode::Left => write!(f, "<LEFT>"),
                    KeyCode::Right => write!(f, "<RIGHT>"),
                    KeyCode::Enter => write!(f, "<ENTER>"),
                    KeyCode::Escape => write!(f, "<ESCAPE>"),
                    KeyCode::Backspace => write!(f, "<BACKSPACE>"),
                    KeyCode::Tab => write!(f, "<TAB>"),
                    KeyCode::BackTab => write!(f, "<BACKTAB>"),
                    KeyCode::Delete => write!(f, "<DELETE>"),
                    KeyCode::Insert => write!(f, "<INSERT>"),
                    KeyCode::Home => write!(f, "<HOME>"),
                    KeyCode::End => write!(f, "<END>"),
                    KeyCode::PageUp => write!(f, "<PAGEUP>"),
                    KeyCode::PageDown => write!(f, "<PAGEDOWN>"),
                    KeyCode::Char(ch) if ch.is_control() => write!(f, "0x{:x}", ch as u32),
                    KeyCode::Char(ch) => write!(f, "{ch}"),
                }
            }
            Self::Mouse(mouse) => match mouse {
                MouseEvent::LeftPress => write!(f, "<LEFTCLICK>"),
                MouseEvent::LeftRelease => write!(f, "<LEFTRELEASE>"),
                MouseEvent::RightPress => write!(f, "<RIGHTCLICK>"),
                MouseEvent::RightRelease => write!(f, "<RIGHTRELEASE>"),
                MouseEvent::MiddlePress => write!(f, "<MIDDLECLICK>"),
                MouseEvent::MiddleRelease => write!(f, "<MIDDLERELEASE>"),
                MouseEvent::Drag => write!(f, "<DRAG>"),
                MouseEvent::ScrollUp => write!(f, "<SCROLLUP>"),
                MouseEvent::ScrollDown => write!(f, "<SCROLLDOWN>"),
            },
        }
    }
}

impl nojson::DisplayJson for InputMatcher {
    fn fmt(&self, f: &mut nojson::JsonFormatter<'_, '_>) -> std::fmt::Result {
        f.string(self)
    }
}