console_lib/
lib.rs

1use std::error::Error;
2use std::ffi::c_int;
3use std::fmt::{Display, Formatter};
4use std::sync::{Mutex, MutexGuard};
5
6#[cfg(feature = "custom_panic_hook")]
7use std::sync::Once;
8
9mod bindings {
10    use std::ffi::{c_char, c_int};
11
12    unsafe extern "C" {
13        pub fn clrscr();
14
15        pub fn initConsole();
16        pub fn reset();
17
18        pub fn getConsoleSize(columns_ret: *mut c_int, rows_ret: *mut c_int);
19
20        pub fn hasInput() -> c_int;
21        pub fn getKey() -> c_int;
22
23        pub fn getMousePosClicked(column: *mut c_int, row: *mut c_int);
24
25        pub fn drawText(text: *const c_char);
26
27        pub fn setColor(fg: c_int, bg: c_int);
28        pub fn resetColor();
29
30        pub fn setUnderline(underline: c_int);
31
32        pub fn setCursorPos(x: c_int, y: c_int);
33    }
34}
35
36/// An abstraction for the console lib which automatically changes the console / terminal mode
37/// in [Console::new] and in the [Drop] implementation of [Console].
38pub struct Console<'a> {
39    _lock: MutexGuard<'a, ()>
40}
41
42static CONSOLE_MUTEX: Mutex<()> = Mutex::new(());
43
44#[cfg(feature = "custom_panic_hook")]
45static CONSOLE_PANIC_HOOK: Once = Once::new();
46
47impl Console<'_> {
48    /// Creates a new console lib abstraction.
49    ///
50    /// The [Console::new] method changes the console / terminal mode (Like disabling text echo).
51    ///
52    /// The [Drop] implementation of [Console] will reset the console / terminal to the original
53    /// state it was in prior to the creation of [Console].
54    ///
55    /// If a panic occurred if the console / terminal mode was already changed, the panic error message would be lost,
56    /// because the [Drop] implementation of [Console] would be called after the panic message was printed.
57    ///
58    /// Because of the mode changes there can only be one instance of a Console struct at once,
59    /// the [Err] variant is returned if there exists another instance of Console.
60    ///
61    /// # Custom Panic Hook
62    ///
63    /// With the `custom_panic_hook` feature the lost panic error message problem can be prevented.
64    /// If the `custom_panic_hook` feature is enabled a panic hook will be created after the first
65    /// time the console / terminal mode is changed inside the [Console::new] method -
66    /// the newly set panic hook persists until the whole program is terminated.
67    /// When a panic occurs, the panic hook checks if a Console instance is still present.
68    /// If this is the case, the console / terminal mode will be reset to the original state.
69    /// Afterward the standard panic hook will be run which will print the panic error message.
70    ///
71    /// If the `custom_panic_hook` feature is enabled and the program is panicking,
72    /// the [Drop] implementation of [Console] would not reset the console / terminal mode again.
73    pub fn new() -> Result<Self, Box<ConsoleError>> {
74        let lock = match CONSOLE_MUTEX.try_lock() {
75            Ok(lock) => lock,
76            Err(_) => {
77                return Err(Box::new(ConsoleError::new("Only one instance of Console can exist at once!")));
78            },
79        };
80
81        unsafe { bindings::initConsole() };
82
83        #[cfg(feature = "custom_panic_hook")]
84        {
85            CONSOLE_PANIC_HOOK.call_once(|| {
86                let default_panic_hook = std::panic::take_hook();
87                std::panic::set_hook(Box::new(move |panic_info| {
88                    //Reset Console before printing panic message if console was initialized (= CONSOLE_MUTEX is locked)
89                    if CONSOLE_MUTEX.try_lock().is_err() {
90                        unsafe { bindings::reset() };
91                    }
92
93                    default_panic_hook(panic_info);
94                }));
95            });
96        }
97
98        Ok(Self { _lock: lock })
99    }
100
101    /// Repaints the screen
102    pub fn repaint(&self) {
103        unsafe { bindings::clrscr() }
104    }
105
106    /// Returns the size of the console in characters as (width, rows).
107    ///
108    /// At the moment Console / Terminal resizing is currently not supported.
109    /// The size is read once after the console is initialized and internal
110    /// buffers are allocated for that size.
111    pub fn get_console_size(&self) -> (usize, usize) {
112        let mut columns_int: c_int = -1;
113        let mut rows_int: c_int = -1;
114
115        unsafe { bindings::getConsoleSize(&mut columns_int, &mut rows_int) }
116
117        (columns_int as usize, rows_int as usize)
118    }
119
120    /// Checks if key input is available
121    pub fn has_input(&self) -> bool {
122        unsafe { bindings::hasInput() != 0 }
123    }
124
125    /// Returns the key which was pressed or None
126    pub fn get_key(&self) -> Option<Key> {
127        let key = unsafe { bindings::getKey() as i32 };
128
129        if key < 0 {
130            None
131        }else {
132            Some(Key(key as u16))
133        }
134    }
135
136    /// Returns the coordinates of the pos where a left click occurred as (x, y).
137    ///
138    /// x and y represent character positions.
139    ///
140    /// If None, no left click occurred.
141    pub fn get_mouse_pos_clicked(&self) -> Option<(usize, usize)> {
142        let mut column_int: c_int = -1;
143        let mut row_int: c_int = -1;
144
145        unsafe { bindings::getMousePosClicked(&mut column_int, &mut row_int) }
146
147        if column_int < 0 || row_int < 0 {
148            None
149        }else {
150            Some((column_int as usize, row_int as usize))
151        }
152    }
153
154    /// Draws text at the current cursor position.
155    ///
156    /// Behavior for Non-ASCII strings is terminal dependent.
157    ///
158    /// Characters which are out of bounds will be ignored and not drawn.
159    pub fn draw_text(&self, text: impl Into<String>) {
160        let text = std::ffi::CString::new(text.into()).unwrap();
161
162        unsafe { bindings::drawText(text.as_ptr()) }
163    }
164
165    /// Sets the color for foreground and background
166    pub fn set_color(&self, fg: Color, bg: Color) {
167        unsafe { bindings::setColor(fg as c_int, bg as c_int) }
168    }
169
170    /// Sets the color for foreground and background
171    ///
172    /// Foreground and background colors are swapped if inverted is true
173    pub fn set_color_invertible(&self, fg: Color, bg: Color, inverted: bool) {
174        if inverted {
175            self.set_color(bg, fg);
176        }else {
177            self.set_color(fg, bg);
178        }
179    }
180
181    /// Resets the color for foreground and background to [Color::Default]
182    pub fn reset_color(&self) {
183        unsafe { bindings::resetColor() }
184    }
185
186    pub fn set_underline(&self, underline: bool) {
187        unsafe { bindings::setUnderline(underline as c_int) }
188    }
189
190    /// Sets the cursor pos to x and y
191    pub fn set_cursor_pos(&self, x: usize, y: usize) {
192        let x = x as c_int;
193        let y = y as c_int;
194
195        if x < 0 || y < 0 {
196            return;
197        }
198
199        unsafe { bindings::setCursorPos(x, y) }
200    }
201}
202
203impl Drop for Console<'_> {
204    fn drop(&mut self) {
205        #[cfg(feature = "custom_panic_hook")]
206        if std::thread::panicking() {
207            //Custom panic hook will call "reset()" instead of this Drop implementation
208            return;
209        }
210
211        unsafe { bindings::reset() };
212    }
213}
214
215/// A representation of a key code from the console lib binding.
216///
217/// The key should be checked with the constants provided in the [Key] implementation (Like [Key::SPACE]).
218///
219/// Unknown keys map to undefined values.
220#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
221pub struct Key(u16);
222
223impl Key {
224    //Ascii
225    pub const SPACE: Key = Key(b' ' as u16);
226    pub const EXCLAMATION_MARK: Key = Key(b'!' as u16);
227    pub const QUOTATION_MARK: Key = Key(b'"' as u16);
228    pub const NUMBER_SIGN: Key = Key(b'#' as u16);
229    pub const DOLLAR: Key = Key(b'$' as u16);
230    pub const PERCENT_SIGN: Key = Key(b'%' as u16);
231    pub const AMPERSAND: Key = Key(b'&' as u16);
232    pub const APOSTROPHE: Key = Key(b'\'' as u16);
233    pub const LEFT_PARENTHESIS: Key = Key(b'(' as u16);
234    pub const RIGHT_PARENTHESIS: Key = Key(b')' as u16);
235    pub const ASTERISK: Key = Key(b'*' as u16);
236    pub const PLUS: Key = Key(b'+' as u16);
237    pub const COMMA: Key = Key(b',' as u16);
238    pub const MINUS: Key = Key(b'-' as u16);
239    pub const DOT: Key = Key(b'.' as u16);
240    pub const SLASH: Key = Key(b'/' as u16);
241
242    pub const COLON: Key = Key(b':' as u16);
243    pub const SEMICOLON: Key = Key(b';' as u16);
244    pub const LESS_THAN_SIGN: Key = Key(b'<' as u16);
245    pub const EQUALS_SIGN: Key = Key(b'=' as u16);
246    pub const GREATER_THAN_SIGN: Key = Key(b'>' as u16);
247    pub const QUESTION_MARK: Key = Key(b'?' as u16);
248    pub const AT_SIGN: Key = Key(b'@' as u16);
249
250    pub const LEFT_BRACKET: Key = Key(b'[' as u16);
251    pub const BACKSLASH: Key = Key(b'\\' as u16);
252    pub const RIGHT_BRACKET: Key = Key(b']' as u16);
253    pub const CARET: Key = Key(b'^' as u16);
254    pub const UNDERSCORE: Key = Key(b'_' as u16);
255    pub const BACKTICK: Key = Key(b'`' as u16);
256
257    pub const LEFT_CURLY_BRACKET: Key = Key(b'{' as u16);
258    pub const VERTICAL_BAR: Key = Key(b'|' as u16);
259    pub const RIGHT_CURLY_BRACKET: Key = Key(b'}' as u16);
260    pub const TILDE: Key = Key(b'~' as u16);
261
262    pub const DIGIT_0: Key = Key(b'0' as u16);
263    pub const DIGIT_1: Key = Key(b'1' as u16);
264    pub const DIGIT_2: Key = Key(b'2' as u16);
265    pub const DIGIT_3: Key = Key(b'3' as u16);
266    pub const DIGIT_4: Key = Key(b'4' as u16);
267    pub const DIGIT_5: Key = Key(b'5' as u16);
268    pub const DIGIT_6: Key = Key(b'6' as u16);
269    pub const DIGIT_7: Key = Key(b'7' as u16);
270    pub const DIGIT_8: Key = Key(b'8' as u16);
271    pub const DIGIT_9: Key = Key(b'9' as u16);
272
273    pub const A: Key = Key(b'a' as u16);
274    pub const B: Key = Key(b'b' as u16);
275    pub const C: Key = Key(b'c' as u16);
276    pub const D: Key = Key(b'd' as u16);
277    pub const E: Key = Key(b'e' as u16);
278    pub const F: Key = Key(b'f' as u16);
279    pub const G: Key = Key(b'g' as u16);
280    pub const H: Key = Key(b'h' as u16);
281    pub const I: Key = Key(b'i' as u16);
282    pub const J: Key = Key(b'j' as u16);
283    pub const K: Key = Key(b'k' as u16);
284    pub const L: Key = Key(b'l' as u16);
285    pub const M: Key = Key(b'm' as u16);
286    pub const N: Key = Key(b'n' as u16);
287    pub const O: Key = Key(b'o' as u16);
288    pub const P: Key = Key(b'p' as u16);
289    pub const Q: Key = Key(b'q' as u16);
290    pub const R: Key = Key(b'r' as u16);
291    pub const S: Key = Key(b's' as u16);
292    pub const T: Key = Key(b't' as u16);
293    pub const U: Key = Key(b'u' as u16);
294    pub const V: Key = Key(b'v' as u16);
295    pub const W: Key = Key(b'w' as u16);
296    pub const X: Key = Key(b'x' as u16);
297    pub const Y: Key = Key(b'y' as u16);
298    pub const Z: Key = Key(b'z' as u16);
299
300    //Arrow keys
301    pub const LEFT: Key = Key(5000);
302    pub const UP: Key = Key(5001);
303    pub const RIGHT: Key = Key(5002);
304    pub const DOWN: Key = Key(5003);
305
306    //F keys
307    pub const F1: Key = Key(5004);
308    pub const F2: Key = Key(5005);
309    pub const F3: Key = Key(5006);
310    pub const F4: Key = Key(5007);
311    pub const F5: Key = Key(5008);
312    pub const F6: Key = Key(5009);
313    pub const F7: Key = Key(5010);
314    pub const F8: Key = Key(5011);
315    pub const F9: Key = Key(5012);
316    pub const F10: Key = Key(5013);
317    pub const F11: Key = Key(5014);
318    pub const F12: Key = Key(5015);
319
320    //Other keys
321    pub const ESC: Key = Key(5016);
322    pub const DELETE: Key = Key(5017);
323    pub const ENTER: Key = Key(5018);
324    pub const TAB: Key = Key(5019);
325}
326
327impl Key {
328    pub fn is_arrow_key(&self) -> bool {
329        (Key::LEFT..=Key::DOWN).contains(self)
330    }
331
332    /// Converts the keycode to an ASCII character if the key represents an ASCII character.
333    pub fn to_ascii(&self) -> Option<u8> {
334        self.is_ascii().then_some(self.0 as u8)
335    }
336    
337    pub fn is_ascii(&self) -> bool {
338        (0..=127).contains(&self.0)
339    }
340
341    /// Checks if a keycode is ASCII and numeric.
342    pub fn is_numeric(&self) -> bool {
343        self.is_ascii() && (self.0 as u8 as char).is_numeric()
344    }
345
346    /// Checks if a keycode is ASCII and alphanumeric.
347    pub fn is_alphanumeric(&self) -> bool {
348        self.is_ascii() && (self.0 as u8 as char).is_alphanumeric()
349    }
350}
351
352/// 4-bit ANSI Color definitions for the native console lib bindings.
353#[repr(i8)]
354#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
355pub enum Color {
356    Black,
357    Blue,
358    Green,
359    Cyan,
360    Red,
361    Pink,
362    Yellow,
363    White,
364    LightBlack,
365    LightBlue,
366    LightGreen,
367    LightCyan,
368    LightRed,
369    LightPink,
370    LightYellow,
371    LightWhite,
372
373    /// Default color is [Color::Black] on unix and default color attributes on Windows.
374    Default = -1
375}
376
377/// An error that occurred during creation of the [Console] struct.
378#[derive(Debug)]
379pub struct ConsoleError {
380    message: String
381}
382
383impl ConsoleError {
384    fn new(message: impl Into<String>) -> Self {
385        Self { message: message.into() }
386    }
387}
388
389impl Display for ConsoleError {
390    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
391        f.write_str(&self.message)
392    }
393}
394
395impl Error for ConsoleError {}