terminal_graphics_interactive 0.2.0

Keyboard and mouse input for terminal_graphics: build interactive terminal games, art and animations. Almost no unsafe.
Documentation
//! The input events a terminal can send: key presses and mouse activity.

/// Modifier keys held down during an event.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Modifiers {
    /// Shift was held.
    pub shift: bool,
    /// Alt (Option on macOS) was held.
    pub alt: bool,
    /// Ctrl was held.
    pub ctrl: bool,
}

impl Modifiers {
    /// No modifiers held.
    pub const NONE: Modifiers = Modifiers {
        shift: false,
        alt: false,
        ctrl: false,
    };

    /// Whether no modifier is held.
    pub fn is_none(self) -> bool {
        self == Modifiers::NONE
    }
}

/// Which key was pressed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum KeyCode {
    /// A character key. Ctrl+C arrives as `Char('c')` with `ctrl` set.
    Char(char),
    /// Enter / Return.
    Enter,
    /// Tab.
    Tab,
    /// Backspace.
    Backspace,
    /// Escape.
    Esc,
    /// Arrow up.
    Up,
    /// Arrow down.
    Down,
    /// Arrow left.
    Left,
    /// Arrow right.
    Right,
    /// Home.
    Home,
    /// End.
    End,
    /// Page Up.
    PageUp,
    /// Page Down.
    PageDown,
    /// Insert.
    Insert,
    /// Delete.
    Delete,
    /// A function key, `F(1)` to `F(12)`.
    F(u8),
}

/// A key press: which key, and which modifiers were held.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Key {
    /// The key.
    pub code: KeyCode,
    /// The modifiers held.
    pub modifiers: Modifiers,
}

impl Key {
    /// A key press with no modifiers.
    pub fn new(code: KeyCode) -> Key {
        Key {
            code,
            modifiers: Modifiers::NONE,
        }
    }

    /// Whether this is the character `c` with no modifiers held.
    pub fn is_char(&self, c: char) -> bool {
        self.code == KeyCode::Char(c) && self.modifiers.is_none()
    }

    /// Whether this is Ctrl plus the character `c`, for example Ctrl+C.
    pub fn is_ctrl(&self, c: char) -> bool {
        self.code == KeyCode::Char(c) && self.modifiers.ctrl
    }
}

/// A mouse button.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MouseButton {
    /// The left button.
    Left,
    /// The middle button (usually the wheel).
    Middle,
    /// The right button.
    Right,
}

/// What the mouse did.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MouseKind {
    /// A button was pressed.
    Down(MouseButton),
    /// A button was released.
    Up(MouseButton),
    /// The mouse moved with a button held.
    Drag(MouseButton),
    /// The mouse moved with no button held.
    Moved,
    /// The wheel scrolled up (away from you).
    ScrollUp,
    /// The wheel scrolled down.
    ScrollDown,
}

/// A mouse event at a position on the terminal screen.
///
/// `x` and `y` are cells counted from `(0, 0)` at the top left of the
/// *terminal*, with `x` to the right and `y` down, the same orientation as
/// [`Display`](terminal_graphics::Display) coordinates.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Mouse {
    /// What happened.
    pub kind: MouseKind,
    /// Column, from 0.
    pub x: isize,
    /// Row, from 0.
    pub y: isize,
    /// The modifiers held.
    pub modifiers: Modifiers,
}

impl Mouse {
    /// The cell of `display` under the mouse, or `None` if the mouse is
    /// outside it. The display is assumed to be drawn with
    /// [`Terminal::draw`](crate::Terminal::draw), which puts its top left cell
    /// at the top left of the terminal, so no offset is needed.
    ///
    /// ```
    /// use terminal_graphics_interactive::{Display, Modifiers, Mouse, MouseKind};
    ///
    /// let display = Display::new(10, 4);
    /// let click = Mouse { kind: MouseKind::Moved, x: 3, y: 2, modifiers: Modifiers::NONE };
    /// assert_eq!(click.cell_in(&display), Some((3, 2)));
    /// ```
    pub fn cell_in(&self, display: &terminal_graphics::Display) -> Option<(isize, isize)> {
        let (width, height) = display.size();
        let inside =
            self.x >= 0 && self.y >= 0 && self.x < width as isize && self.y < height as isize;
        inside.then_some((self.x, self.y))
    }
}

/// One input event.
///
/// More kinds of event may be added in future, so always end a `match` on an
/// `Event` with a `_ => {}` arm.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Event {
    /// A key was pressed (or is repeating while held).
    ///
    /// ```
    /// use terminal_graphics_interactive::{Event, Key, KeyCode};
    ///
    /// let event = Event::Key(Key::new(KeyCode::Char('a')));
    /// match event {
    ///     Event::Key(key) => assert!(key.is_char('a')),
    ///     _ => {}
    /// }
    /// ```
    Key(Key),
    /// A key was released. Only terminals that support the kitty keyboard
    /// protocol send these (kitty, WezTerm, Ghostty, foot, Alacritty, iTerm2,
    /// recent Windows Terminal), and only after the program asks for them with
    /// `Terminal::enable_key_release`. Other terminals never send this event,
    /// so a game should not depend on it. Keys held down send repeats as
    /// [`Event::Key`], not as key releases.
    ///
    /// ```
    /// use terminal_graphics_interactive::{Event, Key, KeyCode};
    ///
    /// let event = Event::KeyUp(Key::new(KeyCode::Left));
    /// match event {
    ///     Event::KeyUp(key) => assert_eq!(key.code, KeyCode::Left),
    ///     _ => {}
    /// }
    /// ```
    KeyUp(Key),
    /// The mouse did something.
    Mouse(Mouse),
    /// Text was pasted. It arrives as one event, not as key presses, so pasting
    /// a command cannot trigger your key bindings. Needs a terminal that
    /// supports bracketed paste (Windows Terminal, iTerm2, most Linux terminals);
    /// others send pasted text as ordinary key presses.
    Paste(String),
}

#[cfg(test)]
mod tests {
    use super::*;
    use terminal_graphics::Display;

    fn mouse_at(x: isize, y: isize) -> Mouse {
        Mouse {
            kind: MouseKind::Moved,
            x,
            y,
            modifiers: Modifiers::NONE,
        }
    }

    #[test]
    fn cell_in_gives_the_cell_when_the_mouse_is_over_the_display() {
        let display = Display::new(10, 4);
        assert_eq!(mouse_at(0, 0).cell_in(&display), Some((0, 0)));
        assert_eq!(mouse_at(9, 3).cell_in(&display), Some((9, 3)));
    }

    #[test]
    fn cell_in_is_none_outside_the_display() {
        let display = Display::new(10, 4);
        assert_eq!(mouse_at(10, 0).cell_in(&display), None);
        assert_eq!(mouse_at(0, 4).cell_in(&display), None);
        assert_eq!(mouse_at(-1, 0).cell_in(&display), None);
        assert_eq!(mouse_at(0, -1).cell_in(&display), None);
    }

    #[test]
    fn cell_in_is_none_for_an_empty_display() {
        assert_eq!(mouse_at(0, 0).cell_in(&Display::new(0, 0)), None);
    }
}