console_utils/
read.rs

1//! A Cross Platform Read Implementation
2//!
3//! This module provides cross-platform functionality for reading keyboard input,
4//! allowing your console application to handle various key events uniformly.
5
6use std::io;
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/// Contains Windows-specific implementation details for reading keyboard
47/// input. It utilizes the `windows-sys` crate to interact with Windows Console API.
48#[cfg(windows)]
49pub mod windows {
50    use super::Key;
51    use std::io;
52    use std::mem;
53    use windows_sys::Win32::System::Console::{
54        GetStdHandle, ReadConsoleInputW, INPUT_RECORD, KEY_EVENT, KEY_EVENT_RECORD,
55        STD_INPUT_HANDLE,
56    };
57    use windows_sys::Win32::UI::Input::KeyboardAndMouse;
58
59    pub(crate) fn read_key() -> io::Result<Key> {
60        let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
61        let mut buffer: INPUT_RECORD = unsafe { mem::zeroed() };
62
63        let mut events_read: u32 = unsafe { mem::zeroed() };
64
65        loop {
66            let success = unsafe { ReadConsoleInputW(handle, &mut buffer, 1, &mut events_read) };
67            if success == 0 {
68                return Err(io::Error::last_os_error());
69            }
70            if events_read == 0 {
71                return Err(io::Error::new(
72                    io::ErrorKind::Other,
73                    "ReadConsoleInput returned no events, instead of waiting for an event",
74                ));
75            }
76
77            if events_read == 1 && buffer.EventType == KEY_EVENT as u16 {
78                let key_event: KEY_EVENT_RECORD = unsafe { mem::transmute(buffer.Event) };
79
80                if key_event.bKeyDown != 0 {
81                    return match key_event.wVirtualKeyCode {
82                        KeyboardAndMouse::VK_UP => Ok(Key::ArrowUp),
83                        KeyboardAndMouse::VK_DOWN => Ok(Key::ArrowDown),
84                        KeyboardAndMouse::VK_RIGHT => Ok(Key::ArrowRight),
85                        KeyboardAndMouse::VK_LEFT => Ok(Key::ArrowLeft),
86                        KeyboardAndMouse::VK_RETURN => Ok(Key::Enter),
87                        KeyboardAndMouse::VK_TAB => Ok(Key::Tab),
88                        KeyboardAndMouse::VK_BACK => Ok(Key::Backspace),
89                        KeyboardAndMouse::VK_ESCAPE => Ok(Key::Escape),
90                        c => Ok(Key::Char(char::from_u32(c as u32).unwrap_or_default())),
91                    };
92                }
93            }
94        }
95    }
96}
97
98/// Contains Unix-specific implementation details for reading keyboard
99/// input. It uses the `libc` crate to manipulate terminal attributes.
100#[cfg(unix)]
101pub mod unix {
102    use libc::{tcgetattr, tcsetattr, ECHO, ICANON, STDIN_FILENO, TCSANOW};
103    use std::io::{self, Read};
104    use std::mem;
105
106    use super::Key;
107
108    // Disables line buffering.
109    fn disable_line_buffering() -> io::Result<()> {
110        let mut termios = unsafe { mem::zeroed() };
111        if unsafe { tcgetattr(STDIN_FILENO, &mut termios) } != 0 {
112            return Err(io::Error::last_os_error());
113        }
114
115        termios.c_lflag &= !(ICANON | ECHO);
116
117        if unsafe { tcsetattr(STDIN_FILENO, TCSANOW, &termios) } != 0 {
118            return Err(io::Error::last_os_error());
119        }
120
121        Ok(())
122    }
123
124    // Enables line buffering.
125    fn enable_line_buffering() -> io::Result<()> {
126        let mut termios = unsafe { mem::zeroed() };
127        if unsafe { tcgetattr(STDIN_FILENO, &mut termios) } != 0 {
128            return Err(io::Error::last_os_error());
129        }
130
131        termios.c_lflag |= ICANON | ECHO;
132
133        if unsafe { tcsetattr(STDIN_FILENO, TCSANOW, &termios) } != 0 {
134            return Err(io::Error::last_os_error());
135        }
136
137        Ok(())
138    }
139
140    // Reads a key from the console.
141    pub(crate) fn read_key() -> io::Result<Key> {
142        let mut buffer = [0; 3];
143        disable_line_buffering()?;
144        if std::io::stdin().read(&mut buffer).is_ok() {
145            enable_line_buffering()?;
146            match buffer[0] {
147                27 => {
148                    // Arrow key sequence
149                    if buffer[1] == 91 {
150                        match buffer[2] {
151                            65 => Ok(Key::ArrowUp),
152                            66 => Ok(Key::ArrowDown),
153                            67 => Ok(Key::ArrowRight),
154                            68 => Ok(Key::ArrowLeft),
155                            _ => Ok(Key::Unknown),
156                        }
157                    } else {
158                        Ok(Key::Unknown)
159                    }
160                }
161                b'\n' => Ok(Key::Enter),
162                b'\t' => Ok(Key::Tab),
163                127 => Ok(Key::Backspace),
164                c => Ok(Key::Char(c as char)),
165            }
166        } else {
167            Err(io::Error::last_os_error())
168        }
169    }
170}