jag

typing_rust

public

A small typing test program in Rust

Star0
Watch0
84c1613e43daf77884945d572c9f0392efa6e7f2
1
ssh://git@git.jaglekon.de:30/jag/typing_rust.git
src/main.rsRust312 lines6.7 KB
1
use std::{
2
    io,
3
    time::{
4
        Duration,
5
        Instant,
6
    }
7
};
8
9
use rand::prelude::*;
10
11
use crossterm::event::{
12
    self, 
13
    poll,
14
    KeyCode,
15
    KeyEvent,
16
    KeyEventKind,
17
    Event,
18
};
19
20
use ratatui::{
21
    DefaultTerminal, 
22
    buffer::Buffer, 
23
    layout::Rect, 
24
    style::{Stylize, Modifier}, 
25
    symbols::border, 
26
    text::{Line, Text}, 
27
    widgets::{Block, Widget, Paragraph, Wrap},
28
    Frame,
29
};
30
31
fn split_at_char(input: &str, n: usize) -> (&str, &str) {
32
     let split = input
33
            .char_indices()
34
            .nth(n)
35
            .map(|(idx , _)| idx)
36
            .unwrap_or(input.len());
37
38
        input.split_at(split)
39
40
}
41
42
43
#[derive(Debug)]
44
struct TextCombine {
45
    text: String,
46
    len: usize,
47
    pos: usize,
48
    err_n: usize,
49
    err_s: String,
50
}
51
52
impl TextCombine {
53
    fn new() -> Self {
54
        let words = [
55
            "hello",
56
            "how",
57
            "to",
58
            "back",
59
            "big",
60
            "small",
61
            "something",
62
            "glad",
63
            "general",
64
            "tall",
65
            "nail",
66
            "whatever",
67
            "joke",
68
            "feral",
69
            "apple",
70
            "plow"
71
        ];
72
73
        /*let text = words
74
                    .choose_multiple(&mut rand::rng(), 10)
75
                    .cloned()
76
                    .collect::<Vec<_>>()
77
                    .join(" ");*/
78
        let mut rng = rand::rng();
79
80
        let text = (0..60)
81
            .filter_map(|_| words.choose(&mut rng))
82
            .cloned()
83
            .collect::<Vec<_>>()
84
            .join(" ");
85
86
        let len = text.chars().count();
87
88
        Self {
89
            text,
90
            len,
91
            pos: 0,
92
            err_n: 0,
93
            err_s: String::new(),
94
        }
95
96
    }
97
98
    fn curr_char(&self) -> Option<char> {
99
        self.pos_char(self.real_pos())
100
    }
101
102
    fn pos_char(&self, pos: usize) -> Option<char> {
103
        self
104
            .text
105
            .chars()
106
            .nth(pos) 
107
    }
108
109
    fn real_pos(&self) -> usize {
110
        self.pos + self.err_n
111
    }
112
113
    fn back(&mut self) {
114
        if self.err_s.len() > 0 {
115
            self.err_s.pop() ;
116
        } else if self.err_n > 0 {
117
            self.err_n -= 1;
118
        } else if self.pos > 0 {
119
            self.pos -= 1;
120
        }
121
    }
122
}
123
124
#[derive(Debug)]
125
pub struct  Timer {
126
    start: Instant,
127
    duration: Duration,
128
}
129
130
impl Timer {
131
    fn new(duration: Duration) -> Self {
132
        Self {
133
            start: Instant::now(),
134
            duration,
135
        }
136
    }
137
138
    fn remaining (&self) -> Duration {
139
        self.duration.saturating_sub(self.elapsed())
140
    }
141
142
    fn elapsed (&self) -> Duration {
143
        self.start.elapsed()
144
    }
145
}
146
147
#[derive(Debug)]
148
pub struct TypingTest {
149
    status: bool,
150
    errors: usize,
151
    typing: TextCombine,
152
    words: usize,
153
    timer: Timer,
154
}
155
156
impl Default for TypingTest {
157
    fn default() -> Self {
158
        Self {
159
            status: true,
160
            errors: 0,
161
            words: 0,
162
            typing: TextCombine::new(),
163
            timer: Timer::new(Duration::from_secs(30))
164
        }
165
    }
166
}
167
168
impl Widget for &TypingTest {
169
    fn render(self, area: Rect, buf: &mut Buffer) {
170
        let title = Line::from(vec![
171
            " WPM ".into(),
172
            format!("{:4.1}" ,(self.words as f64 / self.timer.elapsed().as_secs_f64()) * 60.0).into(),
173
            " // Time ".into(),
174
            format!("{:2}", (self.timer.remaining().as_secs())).into(),
175
            " // Mistakes ".into(),
176
            self.errors.to_string().into(),
177
            (" ").into()
178
        ]);
179
        let bottom = Line::from(vec![
180
            " Exit ".into(),
181
            "<Esc> ".into(),
182
        ]);
183
184
        let block = Block::bordered()
185
            .title(title.centered().bold())
186
            .title_bottom(bottom.centered().bold())
187
            .border_set(border::THICK);
188
        
189
        let (first, rest) = 
190
            split_at_char(self.typing.text.as_str(), self.typing.pos);
191
192
        let (second, rest2) = 
193
            split_at_char(rest, self.typing.err_n);
194
195
        let (third, fourth) =
196
            split_at_char(rest2, 1); 
197
198
        
199
200
        let text = Line::from(vec![
201
            first.green().into(),
202
            second.red().into(),
203
            self.typing.err_s.clone().red().into(),
204
            if third.len() != 0 {
205
                third
206
            } else {
207
                " "
208
            }
209
            .add_modifier(Modifier::UNDERLINED | Modifier::BOLD)
210
            .into(),
211
            fourth.into(),
212
        ]);
213
214
215
        Paragraph::new(text)
216
            .wrap(Wrap {trim: true})
217
            .block(block)
218
            .render(area, buf);
219
    }
220
}
221
222
223
impl TypingTest {
224
225
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
226
227
        while self.status {
228
            terminal.draw(|frame| self.draw(frame))?;
229
            if poll(Duration::from_millis(100))? {
230
                self.handle_events()?;
231
            }
232
        }
233
        
234
        Ok(())
235
    }
236
    
237
    fn draw(&self, frame: &mut Frame) {
238
        frame.render_widget(self, frame.area());
239
    }
240
241
    fn handle_events(&mut self) -> io::Result<()>{
242
        match event::read()? {
243
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
244
                self.handle_key_event(key_event);
245
            }, 
246
            _ => {},
247
        }
248
        Ok(())
249
    }
250
251
    fn handle_key_event(&mut self, key_event: KeyEvent) {
252
        match key_event.code {
253
            KeyCode::Esc => self.exit(),
254
            KeyCode::Backspace => self.back(),
255
            KeyCode::Char(c) => self.handle_char(c),
256
            _ => {},
257
        }
258
    }
259
260
    fn back(&mut self) {
261
262
        if let Some(pos) = self.typing
263
        .pos_char(self.typing.real_pos().saturating_sub(1))
264
        {
265
            if pos == ' ' {return;}
266
        }
267
268
        self.typing.back();
269
    }
270
271
    fn handle_char(&mut self, c: char) {
272
        
273
        if self.typing.real_pos() >= self.typing.len {
274
            return;
275
        }
276
277
        if let Some(curr) = self.typing
278
            .curr_char()
279
        {
280
            
281
            if curr == c 
282
                && self.typing.err_n == 0 
283
                && self.typing.err_s.len() == 0 
284
            {
285
                self.typing.pos += 1;
286
                if c == ' ' {
287
                    self.words += 1;
288
                }
289
            } else if c != ' ' {
290
                if curr != ' ' {
291
                    self.typing.err_n += 1;
292
                    self.errors += 1;
293
                } else if self.typing.err_s.len() < 16 {
294
                    self.typing.err_s.push(c); 
295
                    self.errors += 1;
296
                }
297
            }
298
            
299
        }
300
        
301
    }
302
303
    fn exit(&mut self) {
304
        self.status = false;
305
    }
306
}
307
308
309
310
fn main () -> io::Result<()> {
311
    ratatui::run(|term| TypingTest::default().run(term))
312
}