sshattrick 0.1.1

Hockey in your terminal over SSH.
Documentation
use crate::types::TerminalEvent;
use anyhow::anyhow;
use crossterm::event::{KeyEventKind, KeyModifiers};

pub const CMD_RESIZE: u8 = 0x04;
const SGR_MOUSE_PREFIX: &[u8] = b"\x1b[<";

fn convert_data_to_key_event(data: &[u8]) -> Option<crossterm::event::KeyEvent> {
    let key = match data {
        b"\x1b\x5b\x41" => crossterm::event::KeyCode::Up,
        b"\x1b\x5b\x42" => crossterm::event::KeyCode::Down,
        b"\x1b\x5b\x43" => crossterm::event::KeyCode::Right,
        b"\x1b\x5b\x44" => crossterm::event::KeyCode::Left,
        b"\x03" | b"\x1b" => crossterm::event::KeyCode::Esc,
        b"\x0d" => crossterm::event::KeyCode::Enter,
        b"\x7f" => crossterm::event::KeyCode::Backspace,
        b"\x1b[3~" => crossterm::event::KeyCode::Delete,
        b"\x09" => crossterm::event::KeyCode::Tab,
        x if x.len() == 1 => crossterm::event::KeyCode::Char(data[0] as char),
        _ => return None,
    };
    Some(crossterm::event::KeyEvent::new(
        key,
        crossterm::event::KeyModifiers::empty(),
    ))
}

fn decode_sgr_mouse_input(ansi_code: &[u8]) -> anyhow::Result<(u8, u16, u16)> {
    let ansi_str = std::str::from_utf8(ansi_code).map_err(|_| anyhow!("Invalid UTF-8 sequence"))?;

    if !ansi_str.as_bytes().starts_with(SGR_MOUSE_PREFIX) {
        return Err(anyhow!("Invalid SGR ANSI mouse code"));
    }

    let cb_mod = if ansi_str.ends_with('M') {
        0
    } else if ansi_str.ends_with('m') {
        3
    } else {
        return Err(anyhow!("Invalid SGR ANSI mouse code"));
    };

    let code_body = &ansi_str[3..ansi_str.len() - 1];
    let components: Vec<&str> = code_body.split(';').collect();
    if components.len() != 3 {
        return Err(anyhow!("Invalid SGR ANSI mouse code format"));
    }

    let cb = cb_mod
        + components[0]
            .parse::<u8>()
            .map_err(|_| anyhow!("Failed to parse Cb"))?;
    let cx = components[1]
        .parse::<u16>()
        .map_err(|_| anyhow!("Failed to parse Cx"))?
        - 1;
    let cy = components[2]
        .parse::<u16>()
        .map_err(|_| anyhow!("Failed to parse Cy"))?
        - 1;

    Ok((cb, cx, cy))
}

fn convert_data_to_mouse_event(data: &[u8]) -> Option<crossterm::event::MouseEvent> {
    let (cb, column, row) = decode_sgr_mouse_input(data).ok()?;
    let kind = match cb {
        0 => crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
        1 => crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Middle),
        2 => crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Right),
        3 => crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Left),
        32 => crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left),
        33 => crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Middle),
        34 => crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Right),
        35 => crossterm::event::MouseEventKind::Moved,
        64 => crossterm::event::MouseEventKind::ScrollUp,
        65 => crossterm::event::MouseEventKind::ScrollDown,
        _ => return None,
    };

    Some(crossterm::event::MouseEvent {
        kind,
        column,
        row,
        modifiers: KeyModifiers::empty(),
    })
}

pub fn convert_data_to_terminal_event(data: &[u8]) -> Option<TerminalEvent> {
    if let Some(&[cols, rows]) = data.strip_prefix(&[CMD_RESIZE]) {
        return Some(TerminalEvent::Resize(cols as u16, rows as u16));
    }

    if data.starts_with(SGR_MOUSE_PREFIX) {
        return convert_data_to_mouse_event(data).map(TerminalEvent::Mouse);
    }

    let key = convert_data_to_key_event(data)?;
    if key.kind != KeyEventKind::Press {
        return None;
    }
    Some(TerminalEvent::Key(key))
}