1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
use std::{
thread,
time::{Duration, Instant},
};
mod raw_mode;
pub use raw_mode::{disable as disable_raw_mode, enable as enable_raw_mode, exit as exit_raw_mode};
pub use crossterm::event::{poll, read, Event};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Input {
/// Returns true if the next frame should be skipped due to the previous frame taking too long
pub frame_skip: bool,
pub event: Option<Event>,
}
impl Input {
/// Wait to get an input. When raw mode is disabled, the input will only be registered after enter is pressed. Call [`enable_raw_mode()`] to avoid having to press enter
#[must_use]
pub fn read() -> Self {
Self {
frame_skip: false,
event: read().ok(),
}
}
/// Spend the given duration attempted to get a keyboard press. Returns None if no event found within time limit. This function will always take the given duration to execute, even if an event is captured sooner than intended
#[must_use]
pub fn sleep_for_input(dur: Duration) -> Self {
let now = Instant::now();
let does_event_exist = poll(dur).unwrap_or(false);
let event_elapsed = now.elapsed();
// Poll exits the moment it gets an input. Wait the remaining amount to keep the total time waited consistent
if event_elapsed < dur {
thread::sleep(dur - event_elapsed);
};
Self {
frame_skip: false,
event: if does_event_exist {
Some(read().map_or_else(|_| unreachable!(), |e| e))
} else {
None
},
}
}
/// sleep for `1/fps` given the passed fps argument, taking away the pass `elapsed` value to account for however long it took your gameloop to run. The returned bool is true if `elapsed` was more than `1/fps`, in which case you should attempt to skip the next frame the best that you can
///
/// This function is intended to be inserted into `MainLoopRoot::sleep_and_get_input_data` in [gemini-engine](https://docs.rs/gemini-engine/latest/gemini_engine/gameloop/with_root/trait.MainLoopRoot.html#method.sleep_and_get_input_data)
#[must_use]
pub fn sleep_fps_and_get_input(fps: f32, elapsed: Duration) -> Self {
let mut fps_reciprocal = Duration::from_secs_f32(1.0 / fps);
let frame_skip = if elapsed < fps_reciprocal {
fps_reciprocal -= elapsed;
false
} else {
// fps_reciprocal = Duration::ZERO;
true
};
let mut input = Self::sleep_for_input(fps_reciprocal);
input.frame_skip = frame_skip;
input
}
/// Generate a tuple from the input. Ideal to plug into `gemini-engine`'s `MainLoopRoot::sleep_and_get_input_data`
#[must_use]
pub fn as_tuple(&self) -> (bool, Option<Event>) {
(self.frame_skip, self.event.clone())
}
/// If this input was a `Ctrl+C` (Keyboard Interrupt), exit immediately. Consumes the original Input but returns it immediately, so is intended to be used like this:
///
/// ```no_run
/// # use console_input::keypress::{Input, self};
/// // Will get the input but exit the process if the input was `Ctrl+C`
/// let input = Input::read()
/// .exit_on_kb_interrupt();
/// ```
#[must_use]
pub fn exit_on_kb_interrupt(self) -> Self {
if matches!(
self.event,
Some(Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}))
) {
exit_raw_mode();
}
self
}
}