A small typing test program in Rust
Commit 84c1613e43da
tarball3 files changed+329-113
@@ -650,14 +650,6 @@ dependencies = [
650650
"windows-sys",651651
]652652
653
-[[package]]654
-name = "monkey"655
-version = "0.1.0"656
-dependencies = [657
- "crossterm",658
- "ratatui",659
-]660
-661653
[[package]]662654
name = "nix"663655
version = "0.29.0"@@ -856,7 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
856848
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"857849
dependencies = [858850
"phf_shared",859
- "rand",851
+ "rand 0.8.8",860852
]861853
862854
[[package]]@@ -899,6 +891,15 @@ version = "0.2.0"
899891
source = "registry+https://github.com/rust-lang/crates.io-index"900892
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"901893
894
+[[package]]895
+name = "ppv-lite86"896
+version = "0.2.21"897
+source = "registry+https://github.com/rust-lang/crates.io-index"898
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"899
+dependencies = [900
+ "zerocopy",901
+]902
+902903
[[package]]903904
name = "proc-macro2"904905
version = "1.0.107"@@ -935,7 +936,27 @@ version = "0.8.8"
935936
source = "registry+https://github.com/rust-lang/crates.io-index"936937
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"937938
dependencies = [938
- "rand_core",939
+ "rand_core 0.6.4",940
+]941
+942
+[[package]]943
+name = "rand"944
+version = "0.9.5"945
+source = "registry+https://github.com/rust-lang/crates.io-index"946
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"947
+dependencies = [948
+ "rand_chacha",949
+ "rand_core 0.9.5",950
+]951
+952
+[[package]]953
+name = "rand_chacha"954
+version = "0.9.0"955
+source = "registry+https://github.com/rust-lang/crates.io-index"956
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"957
+dependencies = [958
+ "ppv-lite86",959
+ "rand_core 0.9.5",939960
]940961
941962
[[package]]@@ -944,6 +965,15 @@ version = "0.6.4"
944965
source = "registry+https://github.com/rust-lang/crates.io-index"945966
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"946967
968
+[[package]]969
+name = "rand_core"970
+version = "0.9.5"971
+source = "registry+https://github.com/rust-lang/crates.io-index"972
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"973
+dependencies = [974
+ "getrandom 0.3.4",975
+]976
+947977
[[package]]948978
name = "ratatui"949979
version = "0.30.2"@@ -1428,6 +1458,15 @@ version = "1.20.1"
14281458
source = "registry+https://github.com/rust-lang/crates.io-index"14291459
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"14301460
1461
+[[package]]1462
+name = "typing_rust"1463
+version = "0.1.0"1464
+dependencies = [1465
+ "crossterm",1466
+ "rand 0.9.5",1467
+ "ratatui",1468
+]1469
+14311470
[[package]]14321471
name = "ucd-trie"14331472
version = "0.1.7"@@ -1670,3 +1709,23 @@ name = "wit-bindgen"
16701709
version = "0.57.1"16711710
source = "registry+https://github.com/rust-lang/crates.io-index"16721711
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"1712
+1713
+[[package]]1714
+name = "zerocopy"1715
+version = "0.8.58"1716
+source = "registry+https://github.com/rust-lang/crates.io-index"1717
+checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"1718
+dependencies = [1719
+ "zerocopy-derive",1720
+]1721
+1722
+[[package]]1723
+name = "zerocopy-derive"1724
+version = "0.8.58"1725
+source = "registry+https://github.com/rust-lang/crates.io-index"1726
+checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"1727
+dependencies = [1728
+ "proc-macro2",1729
+ "quote",1730
+ "syn 2.0.119",1731
+]@@ -1,8 +1,9 @@
11
[package]2
-name = "monkey"2
+name = "typing_rust"33
version = "0.1.0"44
edition = "2024"55
66
[dependencies]77
crossterm = "0.29.0"88
ratatui = "0.30.2"9
+rand = "0.9"@@ -1,156 +1,312 @@
1
-use std::io;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
+};219
3
-use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};420
use ratatui::{5
- buffer::Buffer,6
- layout::{Rect, Layout, Direction, Constraint},7
- style::Stylize,8
- symbols::border,9
- text::{Line, Text},10
- widgets::{Block, Paragraph, Widget},11
- DefaultTerminal, Frame,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,1229
};1330
14
-#[derive(Debug, PartialEq, Eq)]15
-enum States {16
- Running,17
- Paused,18
- Quit,31
+fn split_at_char(input: &str, n: usize) -> (&str, &str) {32
+ let split = input33
+ .char_indices()34
+ .nth(n)35
+ .map(|(idx , _)| idx)36
+ .unwrap_or(input.len());37
+38
+ input.split_at(split)39
+1940
}2041
21
-impl Default for States {22
- fn default() -> Self {23
- States::Running42
+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 = words74
+ .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
+ self104
+ .text105
+ .chars()106
+ .nth(pos) 107
+ }108
+109
+ fn real_pos(&self) -> usize {110
+ self.pos + self.err_n111
+ }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
+ }24121
}25122
}26123
27
-#[derive(Debug, Default)]28
-pub struct App {29
- counter: u8,30
- state: States,124
+#[derive(Debug)]125
+pub struct Timer {126
+ start: Instant,127
+ duration: Duration,31128
}32129
33
-impl Widget for &App {34
- fn render(self, area: Rect, buf: &mut Buffer) {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
+}35146
36
- let title = Line::from(" Counter App Tutorial ".bold());37
- let instructions = Line::from(vec![38
- " Decrement ".into(),39
- "<Left>".blue().bold(),40
- " Increment ".into(),41
- "<Right>".blue().bold(),42
- " Pause ".into(),43
- "<P>".blue().bold(),44
- " Quit".into(),45
- "<Q> ".blue().bold(),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(),46182
]);183
+47184
let block = Block::bordered()48
- .title(title.centered())49
- .title_bottom(instructions.centered())185
+ .title(title.centered().bold())186
+ .title_bottom(bottom.centered().bold())50187
.border_set(border::THICK);188
+ 189
+ let (first, rest) = 190
+ split_at_char(self.typing.text.as_str(), self.typing.pos);51191
52
- let counter_text = Text::from(vec![Line::from(vec![53
- "Value: ".into(),54
- self.counter.to_string().yellow(),55
- ])]);192
+ let (second, rest2) = 193
+ split_at_char(rest, self.typing.err_n);56194
57
- Paragraph::new(counter_text)58
- .centered()59
- .block(block)60
- .render(area, buf);195
+ let (third, fourth) =196
+ split_at_char(rest2, 1); 61197
62
- if self.state == States::Paused {63
- 64
- let pause_area = area.centered(65
- Constraint::Length(20),66
- Constraint::Length(5),67
- );198
+ 68199
69
- let pause_block = Block::bordered()70
- .border_set(border::ROUNDED);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
+ third206
+ } else {207
+ " "208
+ }209
+ .add_modifier(Modifier::UNDERLINED | Modifier::BOLD)210
+ .into(),211
+ fourth.into(),212
+ ]);71213
72
- let pause_text = Text::from("PAUSE");73
- 74
- let pause_text_area = pause_area.centered(75
- Constraint::Length(pause_text.width() as u16),76
- Constraint::Length(1),77
- );78
- 79
- pause_block.render(pause_area, buf);80214
81
- Paragraph::new(pause_text)82
- .render(pause_text_area, buf);83
- }215
+ Paragraph::new(text)216
+ .wrap(Wrap {trim: true})217
+ .block(block)218
+ .render(area, buf);84219
}85220
}86221
87
-impl App {88222
89
- pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {90
- while self.state != States::Quit {223
+impl TypingTest {224
+225
+ fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {226
+227
+ while self.status {91228
terminal.draw(|frame| self.draw(frame))?;92
- self.handle_events()?;229
+ if poll(Duration::from_millis(100))? {230
+ self.handle_events()?;231
+ }93232
}233
+ 94234
Ok(())95235
}96
-236
+ 97237
fn draw(&self, frame: &mut Frame) {98238
frame.render_widget(self, frame.area());99239
}100240
101
- fn handle_events(&mut self) -> io::Result<()> {241
+ fn handle_events(&mut self) -> io::Result<()>{102242
match event::read()? {103243
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {104
- self.handle_key_event(key_event)105
- },244
+ self.handle_key_event(key_event);245
+ }, 106246
_ => {},107
- };247
+ }108248
Ok(())109249
}110250
111251
fn handle_key_event(&mut self, key_event: KeyEvent) {112
- match self.state {113
- States::Running => 114
- match key_event.code {115
- KeyCode::Char('p') => self.pause(),116
- KeyCode::Char('q') => self.exit(),117
- KeyCode::Left => self.decrement_counter(),118
- KeyCode::Right => self.increment_counter(),119
- _ => {}120
- },121
- States::Paused => 122
- match key_event.code {123
- KeyCode::Char('p') => self.pause(),124
- KeyCode::Char('q') => self.exit(),125
- _ => {},126
- },127
-128
- States::Quit => {},252
+ match key_event.code {253
+ KeyCode::Esc => self.exit(),254
+ KeyCode::Backspace => self.back(),255
+ KeyCode::Char(c) => self.handle_char(c),256
+ _ => {},129257
}130258
}131259
132
- fn pause(&mut self) {133
- if self.state == States::Paused {134
- self.state = States::Running;135
- } else {136
- self.state = States::Paused;260
+ fn back(&mut self) {261
+262
+ if let Some(pos) = self.typing263
+ .pos_char(self.typing.real_pos().saturating_sub(1))264
+ {265
+ if pos == ' ' {return;}137266
}138
- }139267
140
- fn exit(&mut self) {141
- self.state = States::Quit;268
+ self.typing.back();142269
}143270
144
- fn increment_counter(&mut self) {145
- self.counter += 1;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.typing278
+ .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
+ 146301
}147302
148
- fn decrement_counter(&mut self) {149
- self.counter -= 1;303
+ fn exit(&mut self) {304
+ self.status = false;150305
}151306
}152307
153308
154
-fn main() -> io::Result<()> {155
- ratatui::run(|terminal| App::default().run(terminal))309
+310
+fn main () -> io::Result<()> {311
+ ratatui::run(|term| TypingTest::default().run(term))156312
}