jag

typing_rust

public

A small typing test program in Rust

Star0
Watch0
b05a3c673636550eda6aaa0bd563dd09a5486bd3
1
ssh://git@git.jaglekon.de:30/jag/typing_rust.git
src/main.rsRust355 lines8.5 KB
1
mod text_combine;
2
use text_combine::{TextCombine, Mode};
3
4
mod timer;
5
use timer::Timer;
6
7
use std::{
8
    fmt::format, io, time::Duration,
9
};
10
11
12
use crossterm::event::{
13
    self, 
14
    poll,
15
    KeyCode,
16
    KeyEvent,
17
    KeyEventKind,
18
    Event,
19
};
20
21
use ratatui::{
22
    DefaultTerminal, 
23
    Frame, 
24
    buffer::Buffer, 
25
    layout::{Constraint, Direction, Rect, Layout}, 
26
    style::{Modifier, Stylize}, 
27
    symbols::border, 
28
    text::{Line, Text}, 
29
    widgets::{Block, Clear, Paragraph, Widget, Wrap},
30
};
31
32
fn split_at_char(input: &str, n: usize) -> (&str, &str) {
33
     let split = input
34
            .char_indices()
35
            .nth(n)
36
            .map(|(idx , _)| idx)
37
            .unwrap_or(input.len());
38
39
        input.split_at(split)
40
41
}
42
43
44
#[derive(Debug, PartialEq)]
45
enum Status {
46
    Running,
47
    Menu,
48
    Exit,
49
}
50
51
#[derive(Debug)]
52
pub struct TypingTest {
53
    status: Status,
54
    errors: usize,
55
    typing: TextCombine,
56
    timer: Timer,
57
}
58
59
use Status::*;
60
61
impl Default for TypingTest {
62
    fn default() -> Self {
63
        Self {
64
            status: Running,
65
            errors: 0,
66
            typing: TextCombine::new(30 * 2, Mode::Excerpt),
67
            timer: Timer::new(Duration::from_secs(30))
68
        }
69
    }
70
}
71
72
impl Widget for &TypingTest {
73
    fn render(self, area: Rect, buf: &mut Buffer) {
74
        match self.status {
75
            Running => self.render_game(area, buf),
76
            Menu => self.render_menu(area, buf),
77
            Exit => {},
78
        }
79
    }
80
}
81
82
83
impl TypingTest {
84
85
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
86
87
        while self.status != Exit {
88
            self.game(terminal)?;
89
            self.menu(terminal)?;
90
        }
91
92
        Ok(())
93
    }
94
95
    fn menu(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
96
        while self.status == Menu {
97
            terminal.draw(|frame| self.draw(frame))?;
98
            self.handle_events()?;
99
        }
100
101
        Ok(())
102
    }
103
 
104
    fn game(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
105
        
106
        if self.status != Running {
107
            return Ok(());
108
        }
109
110
        terminal.draw(|frame| self.draw(frame))?;
111
        self.handle_events()?;
112
113
        self.timer.start();
114
115
        while self.status == Running {
116
            if poll(Duration::from_millis(100))? {
117
                self.handle_events()?;
118
            }
119
            terminal.draw(|frame| self.draw(frame))?;
120
            self.check_time();
121
        }
122
123
        Ok(())
124
125
    }
126
127
    fn check_time(&mut self) {
128
        if self.timer.remaining().is_zero() {
129
            self.status = Menu;
130
        }
131
    }
132
133
    fn draw(&self, frame: &mut Frame) {
134
        frame.render_widget(self, frame.area());
135
    }
136
137
    fn handle_events(&mut self) -> io::Result<()>{
138
        match event::read()? {
139
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
140
                match self.status {
141
                    Running => self.handle_game_event(key_event),
142
                    Menu => self.handle_menu_event(key_event),
143
                    Exit => {},
144
                }
145
            }, 
146
            _ => {},
147
        }
148
        Ok(())
149
    }
150
151
    fn handle_game_event(&mut self, key_event: KeyEvent) {
152
        match key_event.code {
153
            KeyCode::Esc => self.exit(),
154
            KeyCode::Backspace => self.back(),
155
            KeyCode::Char(c) => self.handle_char(c),
156
            _ => {},
157
        }
158
    }
159
160
    fn handle_menu_event(&mut self, key_event: KeyEvent) {
161
        match key_event.code {
162
            KeyCode::Esc => self.exit(),
163
            KeyCode::Enter => self.start(),
164
            _ => {}
165
        }
166
    }
167
168
    fn back(&mut self) {
169
170
        if let Some(pos) = self.typing
171
        .pos_char(self.typing.real_pos().saturating_sub(1))
172
        {
173
            if pos == ' ' {return;}
174
        }
175
176
        self.typing.back();
177
    }
178
179
    fn handle_char(&mut self, c: char) {
180
        
181
        if self.typing.real_pos() >= self.typing.len() {
182
            return;
183
        }
184
185
        if let Some(curr) = self.typing
186
            .curr_char()
187
        {
188
            
189
            if curr == c 
190
                && self.typing.get_err_n() == 0 
191
                && self.typing.get_err_s().len() == 0 
192
            {
193
                self.typing.pos_increment();
194
            } else if c != ' ' {
195
                if curr != ' ' {
196
                    self.typing.err_n_increment();
197
                    self.errors += 1;
198
                } else if self.typing.get_err_s().len() < 16 {
199
                    self.typing.push_err_s(c); 
200
                    self.errors += 1;
201
                }
202
            }
203
            
204
        }
205
        
206
    }
207
208
    fn render_menu(&self, area: Rect, buf: &mut Buffer) {
209
        self.render_game(area, buf);
210
211
        let title = Line::from(" Menu ");
212
        let bottom = Line::from( vec![
213
            " New Game ".into(),
214
            "<Enter> ".into(),
215
            "Exit ".into(),
216
            "<Esc> ".into(),
217
        ]);
218
    
219
        let block = Block::bordered()
220
            .title(title.centered().bold())
221
            .title_bottom(bottom.centered().bold())
222
            .border_set(border::THICK);
223
224
        let area_menu = area.centered(
225
            Constraint::Length(40),
226
            Constraint::Length(10),
227
228
        );
229
230
        let block_half = Block::bordered().border_set(border::PLAIN);
231
232
        let split = Layout::default()
233
            .direction(Direction::Horizontal)
234
            .constraints([
235
                Constraint::Percentage(50),
236
                Constraint::Percentage(50),
237
            ])
238
            .split(block.inner(area_menu));
239
        
240
        let wpm = Text::from(vec![
241
            "WPM".into(),
242
            format!("{:4.1}",self.get_wpm()).into(),
243
        ])
244
        .centered()
245
        .bold();
246
247
        let accuarcy = Text::from(vec![
248
            "Accuracy".into(),
249
            format!("{:3.1}", self.get_accuracy()).into(),
250
        ])
251
        .centered()
252
        .bold();
253
254
        let wpm_area = block_half.inner(split[0]).centered(
255
            Constraint::Length(wpm.width() as u16),
256
            Constraint::Length(2),
257
        );
258
259
        let accuarcy_area = block_half.inner(split[1]).centered(
260
            Constraint::Length(accuarcy.width() as u16),
261
            Constraint::Length(2),
262
        );
263
264
265
266
        Clear.render(area_menu, buf);
267
        block_half.clone().render(split[0], buf);
268
        wpm.render(wpm_area, buf);
269
        block_half.render(split[1], buf);
270
        accuarcy.render(accuarcy_area, buf);
271
        block.render(area_menu, buf);
272
        
273
    }
274
275
    fn render_game(&self, area: Rect, buf: &mut Buffer) {
276
        let title = Line::from(vec![
277
            " WPM ".into(),
278
            format!("{:4.1}" , self.get_wpm()).into(),
279
            " // Time ".into(),
280
            if self.timer.remaining().as_secs() < 10 {
281
                format!("{:.1}", self.timer.remaining().as_millis() as f64 / 1000.0).into()
282
            } else {
283
                format!("{:2} ", (self.timer.remaining().as_secs())).into()
284
            },
285
            " // Mistakes ".into(),
286
            self.errors.to_string().into(),
287
            " ".into(),
288
        ]);
289
        let bottom = Line::from(vec![
290
            " Exit ".into(),
291
            "<Esc> ".into(),
292
        ]);
293
294
        let block = Block::bordered()
295
            .title(title.centered().bold())
296
            .title_bottom(bottom.centered().bold())
297
            .border_set(border::THICK);
298
        
299
        let (first, rest) = 
300
            split_at_char(self.typing.get_text(), self.typing.get_pos());
301
302
        let (second, rest2) = 
303
            split_at_char(rest, self.typing.get_err_n());
304
305
        let (third, fourth) =
306
            split_at_char(rest2, 1); 
307
308
        
309
310
        let text = Line::from(vec![
311
            first.green().into(),
312
            second.red().into(),
313
            self.typing.get_err_s().red().into(),
314
            if third.len() != 0 {
315
                third
316
            } else {
317
                " "
318
            }
319
            .add_modifier(Modifier::UNDERLINED | Modifier::BOLD)
320
            .into(),
321
            fourth.into(),
322
        ]);
323
324
325
        Paragraph::new(text)
326
            .wrap(Wrap {trim: true})
327
            .block(block)
328
            .render(area, buf);
329
    }
330
331
332
    fn get_wpm(&self) -> f64 {
333
        (((self.typing.get_pos()) as f64 / 5.0) 
334
            / self.timer.elapsed().as_secs_f64())
335
            * 60.0
336
    }
337
338
    fn get_accuracy(&self) -> f64 {
339
        (1.0 - self.errors as f64 / (self.typing.get_pos() + self.errors) as f64) * 100.0
340
    }
341
342
    fn start(&mut self) {
343
        *self = Self::default();
344
    }
345
346
    fn exit(&mut self) {
347
        self.status = Exit;
348
    }
349
}
350
351
352
353
fn main () -> io::Result<()> {
354
    ratatui::run(|term| TypingTest::default().run(term))
355
}