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
    }
}