fentext_ui/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/piot/fentext-ui
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use crossterm::cursor::MoveTo;
6use crossterm::event::{Event, KeyCode, KeyEventKind};
7use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
8use crossterm::{
9    cursor::{self},
10    event, execute,
11    terminal::{Clear, ClearType, EnterAlternateScreen, enable_raw_mode},
12};
13use std::io;
14use std::io::{Stdout, Write, stdout};
15use std::time::Duration;
16
17#[derive(Eq, PartialEq)]
18pub enum Input {
19    Left,
20    Right,
21    Up,
22    Down,
23    Esc,
24    Action1,
25    Action2,
26    Start,
27}
28
29/// The `Tui` struct gives you direct control over the terminal for your game loop:
30/// - Move the cursor around the screen.
31/// - Write text wherever you need it.
32/// - Get **gamepad-style input** (directional presses and action buttons) without blocking.
33pub struct Tui {
34    out: Stdout,
35}
36
37impl Drop for Tui {
38    fn drop(&mut self) {
39        // When the `Tui` struct is dropped, the terminal is restored to its
40        // original state. This includes showing the cursor, leaving the
41        // alternate screen, and disabling raw mode.
42        //
43        // Errors during cleanup are intentionally ignored here.
44        let _ = execute!(self.out, cursor::Show);
45        let _ = execute!(self.out, LeaveAlternateScreen);
46        let _ = disable_raw_mode();
47    }
48}
49
50impl Tui {
51    /// # Errors
52    ///
53    pub fn new() -> io::Result<Self> {
54        // Enable raw mode: enables key presses without buffering,
55        // and disable automatic echoing of input characters.
56        enable_raw_mode()?;
57
58        let mut stdout = stdout();
59
60        // Save the screen to a buffer that can be restored afterward.
61        execute!(stdout, EnterAlternateScreen)?;
62
63        // Hide the cursor. since we are not using text input in an action based
64        // text game it is best to hide it.
65        execute!(stdout, cursor::Hide)?;
66
67        execute!(stdout, Clear(ClearType::All))?;
68
69        Ok(Self { out: stdout })
70    }
71
72    pub fn move_to(&self, x: u16, y: u16) {
73        execute!(&self.out, MoveTo(x, y)).expect("tui: move_to failed");
74    }
75
76    pub fn write(&self, str: &str) {
77        write!(&self.out, "{str}").expect("tui: write failed");
78    }
79
80    #[must_use]
81    pub fn poll(&self) -> Option<Input> {
82        match event::poll(Duration::ZERO) {
83            Ok(true) => {}
84            Err(_) | Ok(false) => return None,
85        }
86
87        let Ok(event) = event::read() else {
88            return None;
89        };
90
91        let Event::Key(key_event) = event else {
92            return None;
93        };
94
95        if key_event.kind != KeyEventKind::Press {
96            return None;
97        }
98
99        match key_event.code {
100            KeyCode::Esc => Some(Input::Esc),
101            KeyCode::Up => Some(Input::Up),
102            KeyCode::Down => Some(Input::Down),
103            KeyCode::Left => Some(Input::Left),
104            KeyCode::Right => Some(Input::Right),
105            KeyCode::Enter => Some(Input::Start),
106            KeyCode::Char(c) => match c {
107                'w' => Some(Input::Up),
108                'a' => Some(Input::Left),
109                's' => Some(Input::Down),
110                'd' => Some(Input::Right),
111                'z' | 'q' => Some(Input::Action1),
112                'x' | 'e' => Some(Input::Action2),
113                _ => None,
114            },
115            _ => None,
116        }
117    }
118}