console_utils/
read.rs

1//! A Cross Platform Read Implementation
2//!
3//! This module provides functions for reading keys and waiting for key presses until a specified timeout,
4//! allowing your console application to handle keyboard events consistently across platforms.
5
6use std::{io, time::Duration};
7
8/// Represents different keyboard keys that can be captured by the `read_key` function.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Key {
11    /// Arrow up key.
12    ArrowUp,
13    /// Arrow down key.
14    ArrowDown,
15    /// Arrow right key.
16    ArrowRight,
17    /// Arrow left key.
18    ArrowLeft,
19    /// Enter/Return key.
20    Enter,
21    /// Tab key.
22    Tab,
23    /// Backspace key.
24    Backspace,
25    /// Escape key.
26    Escape,
27    /// Any printable character on the keyboard.
28    Char(char),
29    /// Any unrecognized key.
30    Unknown,
31}
32
33/// Reads a single key event from the console input and returns a `Key` enum.
34pub fn read_key() -> io::Result<Key> {
35    #[cfg(windows)]
36    {
37        windows::read_key()
38    }
39
40    #[cfg(unix)]
41    {
42        unix::read_key()
43    }
44}
45
46/// Waits for a key press for up to the specified `timeout` duration.
47pub fn key_pressed_within(timeout: Duration) -> io::Result<Option<Key>> {
48    #[cfg(windows)]
49    {
50        windows::key_pressed_within(timeout)
51    }
52    #[cfg(unix)]
53    {
54        unix::key_pressed_within(timeout)
55    }
56}
57
58/// Contains Windows-specific implementation details for reading keyboard
59/// input. It utilizes the `windows-sys` crate to interact with Windows Console API.
60#[cfg(windows)]
61pub mod windows {
62    use super::Key;
63    use std::io;
64    use std::mem;
65    use std::os::windows::raw::HANDLE;
66    use std::time::Instant;
67    use windows_sys::Win32::Foundation::{INVALID_HANDLE_VALUE, WAIT_OBJECT_0, WAIT_TIMEOUT};
68    use windows_sys::Win32::System::Console::{
69        GetStdHandle, PeekConsoleInputW, ReadConsoleInputW, INPUT_RECORD, KEY_EVENT,
70        KEY_EVENT_RECORD, STD_INPUT_HANDLE,
71    };
72    use windows_sys::Win32::System::Threading::WaitForSingleObject;
73    use windows_sys::Win32::UI::Input::KeyboardAndMouse;
74
75    pub(crate) fn read_key() -> io::Result<Key> {
76        let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
77        let mut buffer: INPUT_RECORD = unsafe { mem::zeroed() };
78
79        let mut events_read: u32 = unsafe { mem::zeroed() };
80
81        loop {
82            let success = unsafe { ReadConsoleInputW(handle, &mut buffer, 1, &mut events_read) };
83            if success == 0 {
84                return Err(io::Error::last_os_error());
85            }
86            if events_read == 0 {
87                return Err(io::Error::new(
88                    io::ErrorKind::Other,
89                    "ReadConsoleInput returned no events, instead of waiting for an event",
90                ));
91            }
92
93            if events_read == 1 && buffer.EventType == KEY_EVENT as u16 {
94                let key_event: KEY_EVENT_RECORD = unsafe { mem::transmute(buffer.Event) };
95
96                if key_event.bKeyDown != 0 {
97                    return match key_event.wVirtualKeyCode {
98                        KeyboardAndMouse::VK_UP => Ok(Key::ArrowUp),
99                        KeyboardAndMouse::VK_DOWN => Ok(Key::ArrowDown),
100                        KeyboardAndMouse::VK_RIGHT => Ok(Key::ArrowRight),
101                        KeyboardAndMouse::VK_LEFT => Ok(Key::ArrowLeft),
102                        KeyboardAndMouse::VK_RETURN => Ok(Key::Enter),
103                        KeyboardAndMouse::VK_TAB => Ok(Key::Tab),
104                        KeyboardAndMouse::VK_BACK => Ok(Key::Backspace),
105                        KeyboardAndMouse::VK_ESCAPE => Ok(Key::Escape),
106                        c => Ok(Key::Char(char::from_u32(c as u32).unwrap_or_default())),
107                    };
108                }
109            }
110        }
111    }
112
113    pub(super) fn key_pressed_within(timeout: std::time::Duration) -> std::io::Result<Option<Key>> {
114        // Peek the next record; if it's noise (non-key or key-up), consume it and return Ok(false).
115        // If a key-down is pending, leave it in the buffer and return Ok(true) so read_key() can take it.
116        unsafe fn ensure_head_is_keydown_or_empty(handle: HANDLE) -> io::Result<bool> {
117            let mut rec: INPUT_RECORD = mem::zeroed();
118            let mut read: u32 = 0;
119
120            // Peek exactly one record; if none, buffer is empty.
121            if PeekConsoleInputW(handle, &mut rec, 1, &mut read) == 0 {
122                return Err(io::Error::last_os_error());
123            }
124            if read == 0 {
125                return Ok(false); // empty buffer
126            }
127
128            if rec.EventType == KEY_EVENT as u16 {
129                // SAFETY: union access matches Win32 layout.
130                let key: KEY_EVENT_RECORD = mem::transmute(rec.Event);
131                if key.bKeyDown != 0 {
132                    // key-down at head; leave it for read_key()
133                    return Ok(true);
134                }
135                // key-up at head: consume it and report "no key-down yet"
136                let mut took: u32 = 0;
137                if ReadConsoleInputW(handle, &mut rec, 1, &mut took) == 0 {
138                    return Err(io::Error::last_os_error());
139                }
140                return Ok(false);
141            }
142
143            // Non-key at head: consume it and report "no key-down yet"
144            let mut took: u32 = 0;
145            if ReadConsoleInputW(handle, &mut rec, 1, &mut took) == 0 {
146                return Err(io::Error::last_os_error());
147            }
148            Ok(false)
149        }
150
151        let handle: HANDLE = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
152        if handle == 0 as HANDLE || handle == INVALID_HANDLE_VALUE {
153            return Err(io::Error::last_os_error());
154        }
155
156        let deadline = Instant::now() + timeout;
157
158        loop {
159            // Clean the head of the queue: remove non-keys and key-ups.
160            // If a key-down is already pending, read it now.
161            if unsafe { ensure_head_is_keydown_or_empty(handle)? } {
162                return Ok(Some(read_key()?));
163            }
164
165            // Remaining time?
166            let now = Instant::now();
167            if now >= deadline {
168                return Ok(None);
169            }
170            let remaining_ms = ((deadline - now).as_millis()).min(u32::MAX as u128) as u32;
171
172            // Wait for *any* console input; when signaled, loop to filter again.
173            match unsafe { WaitForSingleObject(handle, remaining_ms) } {
174                WAIT_TIMEOUT => return Ok(None),
175                WAIT_OBJECT_0 => continue,
176                _ => return Err(io::Error::last_os_error()),
177            }
178        }
179    }
180}
181
182/// Contains Unix-specific implementation details for reading keyboard
183/// input. It uses the `libc` crate to manipulate terminal attributes.
184#[cfg(unix)]
185pub mod unix {
186    use libc::{
187        poll, pollfd, tcgetattr, tcsetattr, termios, ECHO, ICANON, POLLIN, STDIN_FILENO, TCSANOW,
188    };
189    use std::io::{self, Read};
190    use std::mem;
191    use std::time::Duration;
192
193    use super::Key;
194
195    /// Small helper: fetch current termios for a given fd.
196    fn get_termios(fd: i32) -> io::Result<termios> {
197        // SAFETY: zeroed termios is immediately filled by tcgetattr on success.
198        let mut t: termios = unsafe { mem::zeroed() };
199        let rc = unsafe { tcgetattr(fd, &mut t as *mut termios) };
200        if rc != 0 {
201            Err(io::Error::last_os_error())
202        } else {
203            Ok(t)
204        }
205    }
206
207    /// Small helper: set termios for a given fd.
208    fn set_termios(fd: i32, t: &termios) -> io::Result<()> {
209        let rc = unsafe { tcsetattr(fd, TCSANOW, t as *const termios) };
210        if rc != 0 {
211            Err(io::Error::last_os_error())
212        } else {
213            Ok(())
214        }
215    }
216
217    /// RAII guard that disables canonical mode and echo, restoring on drop.
218    struct RawMode {
219        fd: i32,
220        saved: termios,
221    }
222
223    impl RawMode {
224        fn new(fd: i32) -> io::Result<Self> {
225            let mut current = get_termios(fd)?;
226            let saved = current;
227
228            // Disable canonical mode and echo.
229            current.c_lflag &= !(ICANON | ECHO);
230
231            // Optionally, we could tweak VMIN/VTIME for finer control,
232            // but we’ll preserve existing semantics.
233            set_termios(fd, &current)?;
234
235            Ok(Self { fd, saved })
236        }
237    }
238
239    impl Drop for RawMode {
240        fn drop(&mut self) {
241            // Best effort restore; nothing to do on failure in Drop.
242            let _ = set_termios(self.fd, &self.saved);
243        }
244    }
245
246    /// Read a single key assuming we are already in raw/no-echo mode.
247    fn read_key_raw() -> io::Result<Key> {
248        let mut buffer = [0u8; 3];
249
250        // We read up to 3 bytes so we can catch arrow escape sequences.
251        let n = std::io::stdin().read(&mut buffer)?;
252
253        if n == 0 {
254            // EOF or nothing read — treat as unknown
255            return Ok(Key::Unknown);
256        }
257
258        match buffer[0] {
259            27 => {
260                // Escape sequence: try to match ESC [ A/B/C/D
261                // Only proceed if we actually got those extra bytes.
262                if n >= 3 && buffer[1] == b'[' {
263                    match buffer[2] {
264                        b'A' => Ok(Key::ArrowUp),
265                        b'B' => Ok(Key::ArrowDown),
266                        b'C' => Ok(Key::ArrowRight),
267                        b'D' => Ok(Key::ArrowLeft),
268                        _ => Ok(Key::Unknown),
269                    }
270                } else if n == 1 {
271                    Ok(Key::Escape)
272                } else {
273                    Ok(Key::Unknown)
274                }
275            }
276            b'\n' => Ok(Key::Enter),
277            b'\t' => Ok(Key::Tab),
278            127 => Ok(Key::Backspace),
279            c => Ok(Key::Char(c as char)),
280        }
281    }
282
283    // Reads a key from the console, temporarily switching to raw/no-echo.
284    pub(crate) fn read_key() -> io::Result<Key> {
285        let _rm = RawMode::new(STDIN_FILENO)?;
286        read_key_raw()
287    }
288
289    /// Wait up to `timeout` for a key. Returns Some(key) if pressed, None on timeout.
290    /// Echo is disabled during the wait so nothing is visually printed.
291    pub(super) fn key_pressed_within(timeout: Duration) -> io::Result<Option<Key>> {
292        let _rm = RawMode::new(STDIN_FILENO)?;
293
294        let mut fds = pollfd {
295            fd: STDIN_FILENO,
296            events: POLLIN,
297            revents: 0,
298        };
299
300        // Clamp to i32::MAX safely.
301        let ms = timeout.as_millis().min(i32::MAX as u128) as i32;
302
303        let rc = unsafe { poll(&mut fds as *mut pollfd, 1, ms) };
304
305        if rc < 0 {
306            return Err(io::Error::last_os_error());
307        }
308        if rc == 0 {
309            // timeout — nothing was pressed
310            return Ok(None);
311        }
312
313        // Read one key while still in raw mode (guard restores on drop).
314        match read_key_raw() {
315            Ok(k) => Ok(Some(k)),
316            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
317            Err(e) => Err(e),
318        }
319    }
320}