embers-client 0.1.0

Client rendering, input handling, configuration, and scripting support for Embers.
use embers_core::RequestId;
use embers_protocol::{ClientMessage, FloatingRequest, InputRequest, NodeRequest};

use crate::presentation::{NavigationDirection, PresentationModel};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeyEvent {
    Char(char),
    Bytes(Vec<u8>),
    Enter,
    Tab,
    Backspace,
    Escape,
    Ctrl(char),
    Alt(char),
    Up,
    Down,
    Left,
    Right,
    Home,
    End,
    Insert,
    Delete,
    PageUp,
    PageDown,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MouseButton {
    Left,
    Middle,
    Right,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MouseModifiers {
    pub shift: bool,
    pub alt: bool,
    pub ctrl: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MouseEventKind {
    Press(MouseButton),
    Release(Option<MouseButton>),
    Drag(MouseButton),
    WheelUp,
    WheelDown,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MouseEvent {
    pub row: u16,
    pub column: u16,
    pub modifiers: MouseModifiers,
    pub kind: MouseEventKind,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Controller;

impl Controller {
    pub fn map_key(
        &self,
        presentation: &PresentationModel,
        request_id: RequestId,
        key: KeyEvent,
    ) -> Option<ClientMessage> {
        match key {
            KeyEvent::Ctrl(ch) => {
                let ch = ch.to_ascii_lowercase();
                match ch {
                    'h' | 'j' | 'k' | 'l' => {
                        let direction = match ch {
                            'h' => NavigationDirection::Left,
                            'j' => NavigationDirection::Down,
                            'k' => NavigationDirection::Up,
                            'l' => NavigationDirection::Right,
                            _ => unreachable!(),
                        };

                        Some(ClientMessage::Node(NodeRequest::Focus {
                            request_id,
                            session_id: presentation.session_id,
                            node_id: presentation.focus_target(direction)?,
                        }))
                    }
                    _ => input_request(presentation, request_id, vec![ctrl_byte(ch)?]),
                }
            }
            KeyEvent::Alt(ch) if ('1'..='9').contains(&ch) => {
                let index = ch.to_digit(10)?.saturating_sub(1);
                let Some(index_usize) = usize::try_from(index).ok() else {
                    return alt_bytes_request(presentation, request_id, ch);
                };
                if let Some(tabs) = presentation.focused_tabs()
                    && index_usize < tabs.tabs.len()
                {
                    return Some(ClientMessage::Node(NodeRequest::SelectTab {
                        request_id,
                        tabs_node_id: tabs.node_id,
                        index,
                    }));
                }
                alt_bytes_request(presentation, request_id, ch)
            }
            KeyEvent::Alt(ch) => alt_bytes_request(presentation, request_id, ch),
            KeyEvent::Escape => {
                if let Some(floating_id) = presentation.focused_floating_id() {
                    Some(ClientMessage::Floating(FloatingRequest::Close {
                        request_id,
                        floating_id,
                    }))
                } else {
                    input_request(presentation, request_id, vec![0x1b])
                }
            }
            KeyEvent::Char(ch) => {
                input_request(presentation, request_id, ch.to_string().into_bytes())
            }
            KeyEvent::Bytes(bytes) if !bytes.is_empty() => {
                input_request(presentation, request_id, bytes)
            }
            KeyEvent::Tab => input_request(presentation, request_id, b"\t".to_vec()),
            KeyEvent::Enter => input_request(presentation, request_id, b"\r".to_vec()),
            KeyEvent::Backspace => input_request(presentation, request_id, vec![0x7f]),
            KeyEvent::Up => input_request(presentation, request_id, b"\x1b[A".to_vec()),
            KeyEvent::Down => input_request(presentation, request_id, b"\x1b[B".to_vec()),
            KeyEvent::Right => input_request(presentation, request_id, b"\x1b[C".to_vec()),
            KeyEvent::Left => input_request(presentation, request_id, b"\x1b[D".to_vec()),
            KeyEvent::Home => input_request(presentation, request_id, b"\x1b[H".to_vec()),
            KeyEvent::End => input_request(presentation, request_id, b"\x1b[F".to_vec()),
            KeyEvent::Insert => input_request(presentation, request_id, b"\x1b[2~".to_vec()),
            KeyEvent::Delete => input_request(presentation, request_id, b"\x1b[3~".to_vec()),
            KeyEvent::PageUp => input_request(presentation, request_id, b"\x1b[5~".to_vec()),
            KeyEvent::PageDown => input_request(presentation, request_id, b"\x1b[6~".to_vec()),
            KeyEvent::Bytes(_) => None,
        }
    }
}

fn ctrl_byte(ch: char) -> Option<u8> {
    ch.is_ascii()
        .then_some((ch.to_ascii_lowercase() as u8) & 0x1f)
}

fn alt_bytes_request(
    presentation: &PresentationModel,
    request_id: RequestId,
    ch: char,
) -> Option<ClientMessage> {
    let mut encoded = [0; 4];
    let mut bytes = vec![0x1b];
    bytes.extend_from_slice(ch.encode_utf8(&mut encoded).as_bytes());
    input_request(presentation, request_id, bytes)
}

fn input_request(
    presentation: &PresentationModel,
    request_id: RequestId,
    bytes: Vec<u8>,
) -> Option<ClientMessage> {
    Some(ClientMessage::Input(InputRequest::Send {
        request_id,
        buffer_id: presentation.focused_buffer_id()?,
        bytes,
    }))
}