tinycrossterm 0.1.0

Minimal, feature-gated, WASM-compatible subset of crossterm
Documentation
mod parse;
mod types;

pub use types::*;

use std::io::{self, Read};
use std::time::Duration;

/// Reads a single event from the terminal (blocking).
pub fn read() -> io::Result<Event> {
    let mut reader = EventReader::new();
    reader.read_event()
}

/// Checks if an event is available within the given timeout.
///
/// Returns `Ok(true)` if an event is available (a subsequent `read()` won't block).
/// Returns `Ok(false)` if the timeout elapsed.
pub fn poll(timeout: Duration) -> io::Result<bool> {
    poll_stdin(timeout)
}

struct EventReader {
    buf: Vec<u8>,
}

impl EventReader {
    fn new() -> Self {
        Self {
            buf: Vec::with_capacity(64),
        }
    }

    fn read_event(&mut self) -> io::Result<Event> {
        loop {
            // Try to parse what we have
            if let Some((event, consumed)) = parse::parse(&self.buf) {
                self.buf.drain(..consumed);
                return Ok(event);
            }

            // If buffer starts with ESC and we've waited, it's a standalone ESC
            if self.buf.len() == 1
                && self.buf[0] == b'\x1b'
                && !poll_stdin(Duration::from_millis(50))?
            {
                self.buf.clear();
                return Ok(parse::esc_event());
            }

            // Read more bytes
            let mut tmp = [0u8; 64];
            match io::stdin().lock().read(&mut tmp) {
                Ok(0) => {
                    return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "stdin closed"));
                }
                Ok(n) => {
                    self.buf.extend_from_slice(&tmp[..n]);
                }
                Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                    // WASI stdin is non-blocking — wait for data then retry
                    poll_stdin(Duration::from_millis(100))?;
                    continue;
                }
                Err(e) => return Err(e),
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Platform-specific poll
// ---------------------------------------------------------------------------

#[cfg(any(unix, target_os = "wasi"))]
fn poll_stdin(timeout: Duration) -> io::Result<bool> {
    use std::os::fd::AsRawFd;

    let fd = io::stdin().as_raw_fd();
    let millis = timeout.as_millis().min(i32::MAX as u128) as i32;

    unsafe {
        let mut pfd = libc::pollfd {
            fd,
            events: libc::POLLIN,
            revents: 0,
        };
        let ret = libc::poll(&mut pfd, 1, millis);
        if ret < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(ret > 0 && (pfd.revents & libc::POLLIN) != 0)
    }
}

#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
fn poll_stdin(timeout: Duration) -> io::Result<bool> {
    // Non-WASI wasm targets generally don't expose a pollable stdin.
    let _ = timeout;
    Ok(true)
}