jag

typing_rust

public

A small typing test program in Rust

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